mirror of https://github.com/django/django.git
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
|
model, field, field_type, field.db_comment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Add any field index and index_together's (deferred as SQLite
|
# Add any field index (deferred as SQLite _remake_table needs it).
|
||||||
# _remake_table needs it).
|
|
||||||
self.deferred_sql.extend(self._model_indexes_sql(model))
|
self.deferred_sql.extend(self._model_indexes_sql(model))
|
||||||
|
|
||||||
# Make M2M tables
|
# Make M2M tables
|
||||||
|
@ -1585,8 +1584,8 @@ class BaseDatabaseSchemaEditor:
|
||||||
|
|
||||||
def _model_indexes_sql(self, model):
|
def _model_indexes_sql(self, model):
|
||||||
"""
|
"""
|
||||||
Return a list of all index SQL statements (field indexes,
|
Return a list of all index SQL statements (field indexes, Meta.indexes)
|
||||||
index_together, Meta.indexes) for the specified model.
|
for the specified model.
|
||||||
"""
|
"""
|
||||||
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
|
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
|
||||||
return []
|
return []
|
||||||
|
@ -1594,11 +1593,6 @@ class BaseDatabaseSchemaEditor:
|
||||||
for field in model._meta.local_fields:
|
for field in model._meta.local_fields:
|
||||||
output.extend(self._field_indexes_sql(model, field))
|
output.extend(self._field_indexes_sql(model, field))
|
||||||
|
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
for field_names in model._meta.index_together:
|
|
||||||
fields = [model._meta.get_field(field) for field in field_names]
|
|
||||||
output.append(self._create_index_sql(model, fields=fields, suffix="_idx"))
|
|
||||||
|
|
||||||
for index in model._meta.indexes:
|
for index in model._meta.indexes:
|
||||||
if (
|
if (
|
||||||
not index.contains_expressions
|
not index.contains_expressions
|
||||||
|
|
|
@ -180,14 +180,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
for unique in model._meta.unique_together
|
for unique in model._meta.unique_together
|
||||||
]
|
]
|
||||||
|
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
# Work out the new value for index_together, taking renames into
|
|
||||||
# account
|
|
||||||
index_together = [
|
|
||||||
[rename_mapping.get(n, n) for n in index]
|
|
||||||
for index in model._meta.index_together
|
|
||||||
]
|
|
||||||
|
|
||||||
indexes = model._meta.indexes
|
indexes = model._meta.indexes
|
||||||
if delete_field:
|
if delete_field:
|
||||||
indexes = [
|
indexes = [
|
||||||
|
@ -210,7 +202,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
"app_label": model._meta.app_label,
|
"app_label": model._meta.app_label,
|
||||||
"db_table": model._meta.db_table,
|
"db_table": model._meta.db_table,
|
||||||
"unique_together": unique_together,
|
"unique_together": unique_together,
|
||||||
"index_together": index_together, # RemovedInDjango51Warning.
|
|
||||||
"indexes": indexes,
|
"indexes": indexes,
|
||||||
"constraints": constraints,
|
"constraints": constraints,
|
||||||
"apps": apps,
|
"apps": apps,
|
||||||
|
@ -226,7 +217,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
"app_label": model._meta.app_label,
|
"app_label": model._meta.app_label,
|
||||||
"db_table": "new__%s" % strip_quotes(model._meta.db_table),
|
"db_table": "new__%s" % strip_quotes(model._meta.db_table),
|
||||||
"unique_together": unique_together,
|
"unique_together": unique_together,
|
||||||
"index_together": index_together, # RemovedInDjango51Warning.
|
|
||||||
"indexes": indexes,
|
"indexes": indexes,
|
||||||
"constraints": constraints,
|
"constraints": constraints,
|
||||||
"apps": apps,
|
"apps": apps,
|
||||||
|
|
|
@ -190,14 +190,12 @@ class MigrationAutodetector:
|
||||||
self.generate_renamed_indexes()
|
self.generate_renamed_indexes()
|
||||||
# Generate removal of foo together.
|
# Generate removal of foo together.
|
||||||
self.generate_removed_altered_unique_together()
|
self.generate_removed_altered_unique_together()
|
||||||
self.generate_removed_altered_index_together() # RemovedInDjango51Warning.
|
|
||||||
# Generate field operations.
|
# Generate field operations.
|
||||||
self.generate_removed_fields()
|
self.generate_removed_fields()
|
||||||
self.generate_added_fields()
|
self.generate_added_fields()
|
||||||
self.generate_altered_fields()
|
self.generate_altered_fields()
|
||||||
self.generate_altered_order_with_respect_to()
|
self.generate_altered_order_with_respect_to()
|
||||||
self.generate_altered_unique_together()
|
self.generate_altered_unique_together()
|
||||||
self.generate_altered_index_together() # RemovedInDjango51Warning.
|
|
||||||
self.generate_added_indexes()
|
self.generate_added_indexes()
|
||||||
self.generate_added_constraints()
|
self.generate_added_constraints()
|
||||||
self.generate_altered_db_table()
|
self.generate_altered_db_table()
|
||||||
|
@ -584,7 +582,7 @@ class MigrationAutodetector:
|
||||||
possible).
|
possible).
|
||||||
|
|
||||||
Defer any model options that refer to collections of fields that might
|
Defer any model options that refer to collections of fields that might
|
||||||
be deferred (e.g. unique_together, index_together).
|
be deferred (e.g. unique_together).
|
||||||
"""
|
"""
|
||||||
old_keys = self.old_model_keys | self.old_unmanaged_keys
|
old_keys = self.old_model_keys | self.old_unmanaged_keys
|
||||||
added_models = self.new_model_keys - old_keys
|
added_models = self.new_model_keys - old_keys
|
||||||
|
@ -608,12 +606,10 @@ class MigrationAutodetector:
|
||||||
if getattr(field.remote_field, "through", None):
|
if getattr(field.remote_field, "through", None):
|
||||||
related_fields[field_name] = field
|
related_fields[field_name] = field
|
||||||
|
|
||||||
# Are there indexes/unique|index_together to defer?
|
# Are there indexes/unique_together to defer?
|
||||||
indexes = model_state.options.pop("indexes")
|
indexes = model_state.options.pop("indexes")
|
||||||
constraints = model_state.options.pop("constraints")
|
constraints = model_state.options.pop("constraints")
|
||||||
unique_together = model_state.options.pop("unique_together", None)
|
unique_together = model_state.options.pop("unique_together", None)
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
index_together = model_state.options.pop("index_together", None)
|
|
||||||
order_with_respect_to = model_state.options.pop(
|
order_with_respect_to = model_state.options.pop(
|
||||||
"order_with_respect_to", None
|
"order_with_respect_to", None
|
||||||
)
|
)
|
||||||
|
@ -742,16 +738,6 @@ class MigrationAutodetector:
|
||||||
),
|
),
|
||||||
dependencies=related_dependencies,
|
dependencies=related_dependencies,
|
||||||
)
|
)
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
if index_together:
|
|
||||||
self.add_operation(
|
|
||||||
app_label,
|
|
||||||
operations.AlterIndexTogether(
|
|
||||||
name=model_name,
|
|
||||||
index_together=index_together,
|
|
||||||
),
|
|
||||||
dependencies=related_dependencies,
|
|
||||||
)
|
|
||||||
# Fix relationships if the model changed from a proxy model to a
|
# Fix relationships if the model changed from a proxy model to a
|
||||||
# concrete model.
|
# concrete model.
|
||||||
relations = self.to_state.relations
|
relations = self.to_state.relations
|
||||||
|
@ -832,8 +818,6 @@ class MigrationAutodetector:
|
||||||
related_fields[field_name] = field
|
related_fields[field_name] = field
|
||||||
# Generate option removal first
|
# Generate option removal first
|
||||||
unique_together = model_state.options.pop("unique_together", None)
|
unique_together = model_state.options.pop("unique_together", None)
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
index_together = model_state.options.pop("index_together", None)
|
|
||||||
if unique_together:
|
if unique_together:
|
||||||
self.add_operation(
|
self.add_operation(
|
||||||
app_label,
|
app_label,
|
||||||
|
@ -842,15 +826,6 @@ class MigrationAutodetector:
|
||||||
unique_together=None,
|
unique_together=None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
if index_together:
|
|
||||||
self.add_operation(
|
|
||||||
app_label,
|
|
||||||
operations.AlterIndexTogether(
|
|
||||||
name=model_name,
|
|
||||||
index_together=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# Then remove each related field
|
# Then remove each related field
|
||||||
for name in sorted(related_fields):
|
for name in sorted(related_fields):
|
||||||
self.add_operation(
|
self.add_operation(
|
||||||
|
@ -1525,10 +1500,6 @@ class MigrationAutodetector:
|
||||||
def generate_removed_altered_unique_together(self):
|
def generate_removed_altered_unique_together(self):
|
||||||
self._generate_removed_altered_foo_together(operations.AlterUniqueTogether)
|
self._generate_removed_altered_foo_together(operations.AlterUniqueTogether)
|
||||||
|
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
def generate_removed_altered_index_together(self):
|
|
||||||
self._generate_removed_altered_foo_together(operations.AlterIndexTogether)
|
|
||||||
|
|
||||||
def _generate_altered_foo_together(self, operation):
|
def _generate_altered_foo_together(self, operation):
|
||||||
for (
|
for (
|
||||||
old_value,
|
old_value,
|
||||||
|
@ -1548,10 +1519,6 @@ class MigrationAutodetector:
|
||||||
def generate_altered_unique_together(self):
|
def generate_altered_unique_together(self):
|
||||||
self._generate_altered_foo_together(operations.AlterUniqueTogether)
|
self._generate_altered_foo_together(operations.AlterUniqueTogether)
|
||||||
|
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
def generate_altered_index_together(self):
|
|
||||||
self._generate_altered_foo_together(operations.AlterIndexTogether)
|
|
||||||
|
|
||||||
def generate_altered_db_table(self):
|
def generate_altered_db_table(self):
|
||||||
models_to_check = self.kept_model_keys.union(
|
models_to_check = self.kept_model_keys.union(
|
||||||
self.kept_proxy_keys, self.kept_unmanaged_keys
|
self.kept_proxy_keys, self.kept_unmanaged_keys
|
||||||
|
|
|
@ -341,33 +341,6 @@ class CreateModel(ModelOperation):
|
||||||
managers=self.managers,
|
managers=self.managers,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
elif isinstance(operation, RenameIndex) and operation.old_fields:
|
|
||||||
options_index_together = {
|
|
||||||
fields
|
|
||||||
for fields in self.options.get("index_together", [])
|
|
||||||
if fields != operation.old_fields
|
|
||||||
}
|
|
||||||
if options_index_together:
|
|
||||||
self.options["index_together"] = options_index_together
|
|
||||||
else:
|
|
||||||
self.options.pop("index_together", None)
|
|
||||||
return [
|
|
||||||
CreateModel(
|
|
||||||
self.name,
|
|
||||||
fields=self.fields,
|
|
||||||
options={
|
|
||||||
**self.options,
|
|
||||||
"indexes": [
|
|
||||||
*self.options.get("indexes", []),
|
|
||||||
models.Index(
|
|
||||||
fields=operation.old_fields, name=operation.new_name
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
bases=self.bases,
|
|
||||||
managers=self.managers,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
return super().reduce(operation, app_label)
|
return super().reduce(operation, app_label)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -310,14 +310,13 @@ class ProjectState:
|
||||||
for from_field_name in from_fields
|
for from_field_name in from_fields
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# Fix index/unique_together to refer to the new field.
|
# Fix unique_together to refer to the new field.
|
||||||
options = model_state.options
|
options = model_state.options
|
||||||
for option in ("index_together", "unique_together"):
|
if "unique_together" in options:
|
||||||
if option in options:
|
options["unique_together"] = [
|
||||||
options[option] = [
|
[new_name if n == old_name else n for n in together]
|
||||||
[new_name if n == old_name else n for n in together]
|
for together in options["unique_together"]
|
||||||
for together in options[option]
|
]
|
||||||
]
|
|
||||||
# Fix to_fields to refer to the new field.
|
# Fix to_fields to refer to the new field.
|
||||||
delay = True
|
delay = True
|
||||||
references = get_references(self, model_key, (old_name, found))
|
references = get_references(self, model_key, (old_name, found))
|
||||||
|
|
|
@ -1595,7 +1595,6 @@ class Model(AltersData, metaclass=ModelBase):
|
||||||
if not clash_errors:
|
if not clash_errors:
|
||||||
errors.extend(cls._check_column_name_clashes())
|
errors.extend(cls._check_column_name_clashes())
|
||||||
errors += [
|
errors += [
|
||||||
*cls._check_index_together(),
|
|
||||||
*cls._check_unique_together(),
|
*cls._check_unique_together(),
|
||||||
*cls._check_indexes(databases),
|
*cls._check_indexes(databases),
|
||||||
*cls._check_ordering(),
|
*cls._check_ordering(),
|
||||||
|
@ -1928,36 +1927,6 @@ class Model(AltersData, metaclass=ModelBase):
|
||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
@classmethod
|
|
||||||
def _check_index_together(cls):
|
|
||||||
"""Check the value of "index_together" option."""
|
|
||||||
if not isinstance(cls._meta.index_together, (tuple, list)):
|
|
||||||
return [
|
|
||||||
checks.Error(
|
|
||||||
"'index_together' must be a list or tuple.",
|
|
||||||
obj=cls,
|
|
||||||
id="models.E008",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
elif any(
|
|
||||||
not isinstance(fields, (tuple, list)) for fields in cls._meta.index_together
|
|
||||||
):
|
|
||||||
return [
|
|
||||||
checks.Error(
|
|
||||||
"All 'index_together' elements must be lists or tuples.",
|
|
||||||
obj=cls,
|
|
||||||
id="models.E009",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
else:
|
|
||||||
errors = []
|
|
||||||
for fields in cls._meta.index_together:
|
|
||||||
errors.extend(cls._check_local_fields(fields, "index_together"))
|
|
||||||
return errors
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_unique_together(cls):
|
def _check_unique_together(cls):
|
||||||
"""Check the value of "unique_together" option."""
|
"""Check the value of "unique_together" option."""
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import bisect
|
import bisect
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import warnings
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -11,7 +10,6 @@ from django.db import connections
|
||||||
from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
|
from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
|
||||||
from django.db.models.query_utils import PathInfo
|
from django.db.models.query_utils import PathInfo
|
||||||
from django.utils.datastructures import ImmutableList, OrderedSet
|
from django.utils.datastructures import ImmutableList, OrderedSet
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.text import camel_case_to_spaces, format_lazy
|
from django.utils.text import camel_case_to_spaces, format_lazy
|
||||||
|
@ -43,7 +41,6 @@ DEFAULT_NAMES = (
|
||||||
"proxy",
|
"proxy",
|
||||||
"swappable",
|
"swappable",
|
||||||
"auto_created",
|
"auto_created",
|
||||||
"index_together", # RemovedInDjango51Warning.
|
|
||||||
"apps",
|
"apps",
|
||||||
"default_permissions",
|
"default_permissions",
|
||||||
"select_on_save",
|
"select_on_save",
|
||||||
|
@ -119,7 +116,6 @@ class Options:
|
||||||
self.indexes = []
|
self.indexes = []
|
||||||
self.constraints = []
|
self.constraints = []
|
||||||
self.unique_together = []
|
self.unique_together = []
|
||||||
self.index_together = [] # RemovedInDjango51Warning.
|
|
||||||
self.select_on_save = False
|
self.select_on_save = False
|
||||||
self.default_permissions = ("add", "change", "delete", "view")
|
self.default_permissions = ("add", "change", "delete", "view")
|
||||||
self.permissions = []
|
self.permissions = []
|
||||||
|
@ -205,13 +201,6 @@ class Options:
|
||||||
self.original_attrs[attr_name] = getattr(self, attr_name)
|
self.original_attrs[attr_name] = getattr(self, attr_name)
|
||||||
|
|
||||||
self.unique_together = normalize_together(self.unique_together)
|
self.unique_together = normalize_together(self.unique_together)
|
||||||
self.index_together = normalize_together(self.index_together)
|
|
||||||
if self.index_together:
|
|
||||||
warnings.warn(
|
|
||||||
f"'index_together' is deprecated. Use 'Meta.indexes' in "
|
|
||||||
f"{self.label!r} instead.",
|
|
||||||
RemovedInDjango51Warning,
|
|
||||||
)
|
|
||||||
# App label/class name interpolation for names of constraints and
|
# App label/class name interpolation for names of constraints and
|
||||||
# indexes.
|
# indexes.
|
||||||
if not getattr(cls._meta, "abstract", False):
|
if not getattr(cls._meta, "abstract", False):
|
||||||
|
|
|
@ -345,21 +345,23 @@ Models
|
||||||
``<field name>`` from model ``<model>``.
|
``<field name>`` from model ``<model>``.
|
||||||
* **models.E007**: Field ``<field name>`` has column name ``<column name>``
|
* **models.E007**: Field ``<field name>`` has column name ``<column name>``
|
||||||
that is used by another field.
|
that is used by another field.
|
||||||
* **models.E008**: ``index_together`` must be a list or tuple.
|
* **models.E008**: ``index_together`` must be a list or tuple. *This check
|
||||||
|
appeared before Django 5.1.*
|
||||||
* **models.E009**: All ``index_together`` elements must be lists or tuples.
|
* **models.E009**: All ``index_together`` elements must be lists or tuples.
|
||||||
|
*This check appeared before Django 5.1.*
|
||||||
* **models.E010**: ``unique_together`` must be a list or tuple.
|
* **models.E010**: ``unique_together`` must be a list or tuple.
|
||||||
* **models.E011**: All ``unique_together`` elements must be lists or tuples.
|
* **models.E011**: All ``unique_together`` elements must be lists or tuples.
|
||||||
* **models.E012**: ``constraints/indexes/index_together/unique_together``
|
* **models.E012**: ``constraints/indexes/unique_together`` refers to the
|
||||||
refers to the nonexistent field ``<field name>``.
|
nonexistent field ``<field name>``.
|
||||||
* **models.E013**: ``constraints/indexes/index_together/unique_together``
|
* **models.E013**: ``constraints/indexes/unique_together`` refers to a
|
||||||
refers to a ``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s
|
``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s are not
|
||||||
are not supported for that option.
|
supported for that option.
|
||||||
* **models.E014**: ``ordering`` must be a tuple or list (even if you want to
|
* **models.E014**: ``ordering`` must be a tuple or list (even if you want to
|
||||||
order by only one field).
|
order by only one field).
|
||||||
* **models.E015**: ``ordering`` refers to the nonexistent field, related field,
|
* **models.E015**: ``ordering`` refers to the nonexistent field, related field,
|
||||||
or lookup ``<field name>``.
|
or lookup ``<field name>``.
|
||||||
* **models.E016**: ``constraints/indexes/index_together/unique_together``
|
* **models.E016**: ``constraints/indexes/unique_together`` refers to field
|
||||||
refers to field ``<field_name>`` which is not local to model ``<model>``.
|
``<field_name>`` which is not local to model ``<model>``.
|
||||||
* **models.E017**: Proxy model ``<model>`` contains model fields.
|
* **models.E017**: Proxy model ``<model>`` contains model fields.
|
||||||
* **models.E018**: Autogenerated column name too long for field ``<field>``.
|
* **models.E018**: Autogenerated column name too long for field ``<field>``.
|
||||||
Maximum length is ``<maximum length>`` for database ``<alias>``.
|
Maximum length is ``<maximum length>`` for database ``<alias>``.
|
||||||
|
|
|
@ -247,7 +247,7 @@ Removes the index named ``name`` from the model with ``model_name``.
|
||||||
Renames an index in the database table for the model with ``model_name``.
|
Renames an index in the database table for the model with ``model_name``.
|
||||||
Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields``
|
Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields``
|
||||||
is an iterable of the strings, often corresponding to fields of
|
is an iterable of the strings, often corresponding to fields of
|
||||||
:attr:`~django.db.models.Options.index_together`.
|
``index_together`` (pre-Django 5.1 option).
|
||||||
|
|
||||||
On databases that don't support an index renaming statement (SQLite and MariaDB
|
On databases that don't support an index renaming statement (SQLite and MariaDB
|
||||||
< 10.5.2), the operation will drop and recreate the index, which can be
|
< 10.5.2), the operation will drop and recreate the index, which can be
|
||||||
|
|
|
@ -451,29 +451,6 @@ not be looking at your Django code. For example::
|
||||||
The ``ValidationError`` raised during model validation when the constraint
|
The ``ValidationError`` raised during model validation when the constraint
|
||||||
is violated has the ``unique_together`` error code.
|
is violated has the ``unique_together`` error code.
|
||||||
|
|
||||||
``index_together``
|
|
||||||
------------------
|
|
||||||
|
|
||||||
.. attribute:: Options.index_together
|
|
||||||
|
|
||||||
Sets of field names that, taken together, are indexed::
|
|
||||||
|
|
||||||
index_together = [
|
|
||||||
["pub_date", "deadline"],
|
|
||||||
]
|
|
||||||
|
|
||||||
This list of fields will be indexed together (i.e. the appropriate
|
|
||||||
``CREATE INDEX`` statement will be issued.)
|
|
||||||
|
|
||||||
For convenience, ``index_together`` can be a single list when dealing with a single
|
|
||||||
set of fields::
|
|
||||||
|
|
||||||
index_together = ["pub_date", "deadline"]
|
|
||||||
|
|
||||||
.. deprecated:: 4.2
|
|
||||||
|
|
||||||
Use the :attr:`~Options.indexes` option instead.
|
|
||||||
|
|
||||||
``constraints``
|
``constraints``
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
@ -114,9 +114,8 @@ the new value.
|
||||||
|
|
||||||
.. method:: BaseDatabaseSchemaEditor.alter_index_together(model, old_index_together, new_index_together)
|
.. method:: BaseDatabaseSchemaEditor.alter_index_together(model, old_index_together, new_index_together)
|
||||||
|
|
||||||
Changes a model's :attr:`~django.db.models.Options.index_together` value; this
|
Changes a model's ``index_together`` value; this will add or remove indexes
|
||||||
will add or remove indexes from the model's table until they match the new
|
from the model's table until they match the new value.
|
||||||
value.
|
|
||||||
|
|
||||||
``alter_db_table()``
|
``alter_db_table()``
|
||||||
--------------------
|
--------------------
|
||||||
|
|
|
@ -59,8 +59,8 @@ creating database indexes. Indexes are added to models using the
|
||||||
|
|
||||||
The :class:`~django.db.models.Index` class creates a b-tree index, as if you
|
The :class:`~django.db.models.Index` class creates a b-tree index, as if you
|
||||||
used :attr:`~django.db.models.Field.db_index` on the model field or
|
used :attr:`~django.db.models.Field.db_index` on the model field or
|
||||||
:attr:`~django.db.models.Options.index_together` on the model ``Meta`` class.
|
``index_together`` on the model ``Meta`` class. It can be subclassed to support
|
||||||
It can be subclassed to support different index types, such as
|
different index types, such as
|
||||||
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
|
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
|
||||||
order (ASC/DESC) for the columns of the index.
|
order (ASC/DESC) for the columns of the index.
|
||||||
|
|
||||||
|
|
|
@ -329,8 +329,7 @@ Django 1.5 also includes several smaller improvements worth noting:
|
||||||
session data in a non-default cache.
|
session data in a non-default cache.
|
||||||
|
|
||||||
* Multi-column indexes can now be created on models. Read the
|
* Multi-column indexes can now be created on models. Read the
|
||||||
:attr:`~django.db.models.Options.index_together` documentation for more
|
``index_together`` documentation for more information.
|
||||||
information.
|
|
||||||
|
|
||||||
* During Django's logging configuration verbose Deprecation warnings are
|
* During Django's logging configuration verbose Deprecation warnings are
|
||||||
enabled and warnings are captured into the logging system. Logged warnings
|
enabled and warnings are captured into the logging system. Logged warnings
|
||||||
|
|
|
@ -778,8 +778,8 @@ Models
|
||||||
an error (before that, it would either result in a database error or
|
an error (before that, it would either result in a database error or
|
||||||
incorrect data).
|
incorrect data).
|
||||||
|
|
||||||
* You can use a single list for :attr:`~django.db.models.Options.index_together`
|
* You can use a single list for ``index_together`` (rather than a list of
|
||||||
(rather than a list of lists) when specifying a single set of fields.
|
lists) when specifying a single set of fields.
|
||||||
|
|
||||||
* Custom intermediate models having more than one foreign key to any of the
|
* Custom intermediate models having more than one foreign key to any of the
|
||||||
models participating in a many-to-many relationship are now permitted,
|
models participating in a many-to-many relationship are now permitted,
|
||||||
|
|
|
@ -317,7 +317,7 @@ Migrations
|
||||||
* The new :class:`~django.db.migrations.operations.RenameIndex` operation
|
* The new :class:`~django.db.migrations.operations.RenameIndex` operation
|
||||||
allows renaming indexes defined in the
|
allows renaming indexes defined in the
|
||||||
:attr:`Meta.indexes <django.db.models.Options.indexes>` or
|
:attr:`Meta.indexes <django.db.models.Options.indexes>` or
|
||||||
:attr:`~django.db.models.Options.index_together` options.
|
``index_together`` options.
|
||||||
|
|
||||||
* The migrations autodetector now generates
|
* The migrations autodetector now generates
|
||||||
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
|
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
|
||||||
|
@ -327,7 +327,7 @@ Migrations
|
||||||
* The migrations autodetector now generates
|
* The migrations autodetector now generates
|
||||||
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
|
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
|
||||||
``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the
|
``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the
|
||||||
:attr:`Meta.index_together <django.db.models.Options.index_together>` to the
|
``Meta.index_together`` to the
|
||||||
:attr:`Meta.indexes <django.db.models.Options.indexes>`.
|
:attr:`Meta.indexes <django.db.models.Options.indexes>`.
|
||||||
|
|
||||||
Models
|
Models
|
||||||
|
|
|
@ -477,9 +477,8 @@ Features deprecated in 4.2
|
||||||
``index_together`` option is deprecated in favor of ``indexes``
|
``index_together`` option is deprecated in favor of ``indexes``
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
|
|
||||||
The :attr:`Meta.index_together <django.db.models.Options.index_together>`
|
The ``Meta.index_together`` option is deprecated in favor of the
|
||||||
option is deprecated in favor of the :attr:`~django.db.models.Options.indexes`
|
:attr:`~django.db.models.Options.indexes` option.
|
||||||
option.
|
|
||||||
|
|
||||||
Migrating existing ``index_together`` should be handled as a migration. For
|
Migrating existing ``index_together`` should be handled as a migration. For
|
||||||
example::
|
example::
|
||||||
|
|
|
@ -252,3 +252,5 @@ See :ref:`deprecated-features-4.2` for details on these changes, including how
|
||||||
to remove usage of these features.
|
to remove usage of these features.
|
||||||
|
|
||||||
* The ``BaseUserManager.make_random_password()`` method is removed.
|
* The ``BaseUserManager.make_random_password()`` method is removed.
|
||||||
|
|
||||||
|
* The model's ``Meta.index_together`` option is removed.
|
||||||
|
|
|
@ -3,26 +3,16 @@ from unittest import skipUnless
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models import (
|
from django.db.models import CASCADE, ForeignKey, Index, Q
|
||||||
CASCADE,
|
|
||||||
CharField,
|
|
||||||
DateTimeField,
|
|
||||||
ForeignKey,
|
|
||||||
Index,
|
|
||||||
Model,
|
|
||||||
Q,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.test import (
|
from django.test import (
|
||||||
TestCase,
|
TestCase,
|
||||||
TransactionTestCase,
|
TransactionTestCase,
|
||||||
ignore_warnings,
|
|
||||||
skipIfDBFeature,
|
skipIfDBFeature,
|
||||||
skipUnlessDBFeature,
|
skipUnlessDBFeature,
|
||||||
)
|
)
|
||||||
from django.test.utils import isolate_apps, override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
from .models import Article, ArticleTranslation, IndexedArticle2
|
from .models import Article, ArticleTranslation, IndexedArticle2
|
||||||
|
|
||||||
|
@ -80,21 +70,6 @@ class SchemaIndexesTests(TestCase):
|
||||||
index_sql[0],
|
index_sql[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
@isolate_apps("indexes")
|
|
||||||
def test_index_together_single_list(self):
|
|
||||||
class IndexTogetherSingleList(Model):
|
|
||||||
headline = CharField(max_length=100)
|
|
||||||
pub_date = DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = ["headline", "pub_date"]
|
|
||||||
|
|
||||||
index_sql = connection.schema_editor()._model_indexes_sql(
|
|
||||||
IndexTogetherSingleList
|
|
||||||
)
|
|
||||||
self.assertEqual(len(index_sql), 1)
|
|
||||||
|
|
||||||
def test_columns_list_sql(self):
|
def test_columns_list_sql(self):
|
||||||
index = Index(fields=["headline"], name="whitespace_idx")
|
index = Index(fields=["headline"], name="whitespace_idx")
|
||||||
editor = connection.schema_editor()
|
editor = connection.schema_editor()
|
||||||
|
|
|
@ -5,9 +5,8 @@ from django.core.checks.model_checks import _check_lazy_references
|
||||||
from django.db import connection, connections, models
|
from django.db import connection, connections, models
|
||||||
from django.db.models.functions import Abs, Lower, Round
|
from django.db.models.functions import Abs, Lower, Round
|
||||||
from django.db.models.signals import post_init
|
from django.db.models.signals import post_init
|
||||||
from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
|
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
|
||||||
from django.test.utils import isolate_apps, override_settings, register_lookup
|
from django.test.utils import isolate_apps, override_settings, register_lookup
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyRouter:
|
class EmptyRouter:
|
||||||
|
@ -29,134 +28,6 @@ def get_max_column_name_length():
|
||||||
return (allowed_len, db_alias)
|
return (allowed_len, db_alias)
|
||||||
|
|
||||||
|
|
||||||
@isolate_apps("invalid_models_tests")
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
class IndexTogetherTests(SimpleTestCase):
|
|
||||||
def test_non_iterable(self):
|
|
||||||
class Model(models.Model):
|
|
||||||
class Meta:
|
|
||||||
index_together = 42
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Model.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"'index_together' must be a list or tuple.",
|
|
||||||
obj=Model,
|
|
||||||
id="models.E008",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_non_list(self):
|
|
||||||
class Model(models.Model):
|
|
||||||
class Meta:
|
|
||||||
index_together = "not-a-list"
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Model.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"'index_together' must be a list or tuple.",
|
|
||||||
obj=Model,
|
|
||||||
id="models.E008",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_list_containing_non_iterable(self):
|
|
||||||
class Model(models.Model):
|
|
||||||
class Meta:
|
|
||||||
index_together = [("a", "b"), 42]
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Model.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"All 'index_together' elements must be lists or tuples.",
|
|
||||||
obj=Model,
|
|
||||||
id="models.E009",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pointing_to_missing_field(self):
|
|
||||||
class Model(models.Model):
|
|
||||||
class Meta:
|
|
||||||
index_together = [["missing_field"]]
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Model.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"'index_together' refers to the nonexistent field 'missing_field'.",
|
|
||||||
obj=Model,
|
|
||||||
id="models.E012",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pointing_to_non_local_field(self):
|
|
||||||
class Foo(models.Model):
|
|
||||||
field1 = models.IntegerField()
|
|
||||||
|
|
||||||
class Bar(Foo):
|
|
||||||
field2 = models.IntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = [["field2", "field1"]]
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Bar.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"'index_together' refers to field 'field1' which is not "
|
|
||||||
"local to model 'Bar'.",
|
|
||||||
hint="This issue may be caused by multi-table inheritance.",
|
|
||||||
obj=Bar,
|
|
||||||
id="models.E016",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pointing_to_m2m_field(self):
|
|
||||||
class Model(models.Model):
|
|
||||||
m2m = models.ManyToManyField("self")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = [["m2m"]]
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
Model.check(),
|
|
||||||
[
|
|
||||||
Error(
|
|
||||||
"'index_together' refers to a ManyToManyField 'm2m', but "
|
|
||||||
"ManyToManyFields are not permitted in 'index_together'.",
|
|
||||||
obj=Model,
|
|
||||||
id="models.E013",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pointing_to_fk(self):
|
|
||||||
class Foo(models.Model):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Bar(models.Model):
|
|
||||||
foo_1 = models.ForeignKey(
|
|
||||||
Foo, on_delete=models.CASCADE, related_name="bar_1"
|
|
||||||
)
|
|
||||||
foo_2 = models.ForeignKey(
|
|
||||||
Foo, on_delete=models.CASCADE, related_name="bar_2"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = [["foo_1_id", "foo_2"]]
|
|
||||||
|
|
||||||
self.assertEqual(Bar.check(), [])
|
|
||||||
|
|
||||||
|
|
||||||
# unique_together tests are very similar to index_together tests.
|
|
||||||
@isolate_apps("invalid_models_tests")
|
@isolate_apps("invalid_models_tests")
|
||||||
class UniqueTogetherTests(SimpleTestCase):
|
class UniqueTogetherTests(SimpleTestCase):
|
||||||
def test_non_iterable(self):
|
def test_non_iterable(self):
|
||||||
|
|
|
@ -13,9 +13,8 @@ from django.db.migrations.graph import MigrationGraph
|
||||||
from django.db.migrations.loader import MigrationLoader
|
from django.db.migrations.loader import MigrationLoader
|
||||||
from django.db.migrations.questioner import MigrationQuestioner
|
from django.db.migrations.questioner import MigrationQuestioner
|
||||||
from django.db.migrations.state import ModelState, ProjectState
|
from django.db.migrations.state import ModelState, ProjectState
|
||||||
from django.test import SimpleTestCase, TestCase, ignore_warnings, override_settings
|
from django.test import SimpleTestCase, TestCase, override_settings
|
||||||
from django.test.utils import isolate_lru_cache
|
from django.test.utils import isolate_lru_cache
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
from .models import FoodManager, FoodQuerySet
|
from .models import FoodManager, FoodQuerySet
|
||||||
|
|
||||||
|
@ -4872,592 +4871,6 @@ class AutodetectorTests(BaseAutodetectorTests):
|
||||||
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book")
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book")
|
||||||
|
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
class AutodetectorIndexTogetherTests(BaseAutodetectorTests):
|
|
||||||
book_index_together = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("author", "title")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
book_index_together_2 = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("title", "author")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
book_index_together_3 = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("newfield", models.IntegerField()),
|
|
||||||
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("title", "newfield")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
book_index_together_4 = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("newfield2", models.IntegerField()),
|
|
||||||
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("title", "newfield2")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_empty_index_together(self):
|
|
||||||
"""Empty index_together shouldn't generate a migration."""
|
|
||||||
# Explicitly testing for not specified, since this is the case after
|
|
||||||
# a CreateModel operation w/o any definition on the original model
|
|
||||||
model_state_not_specified = ModelState(
|
|
||||||
"a", "model", [("id", models.AutoField(primary_key=True))]
|
|
||||||
)
|
|
||||||
# Explicitly testing for None, since this was the issue in #23452 after
|
|
||||||
# an AlterIndexTogether operation with e.g. () as value
|
|
||||||
model_state_none = ModelState(
|
|
||||||
"a",
|
|
||||||
"model",
|
|
||||||
[("id", models.AutoField(primary_key=True))],
|
|
||||||
{
|
|
||||||
"index_together": None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# Explicitly testing for the empty set, since we now always have sets.
|
|
||||||
# During removal (('col1', 'col2'),) --> () this becomes set([])
|
|
||||||
model_state_empty = ModelState(
|
|
||||||
"a",
|
|
||||||
"model",
|
|
||||||
[("id", models.AutoField(primary_key=True))],
|
|
||||||
{
|
|
||||||
"index_together": set(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test(from_state, to_state, msg):
|
|
||||||
changes = self.get_changes([from_state], [to_state])
|
|
||||||
if changes:
|
|
||||||
ops = ", ".join(
|
|
||||||
o.__class__.__name__ for o in changes["a"][0].operations
|
|
||||||
)
|
|
||||||
self.fail("Created operation(s) %s from %s" % (ops, msg))
|
|
||||||
|
|
||||||
tests = (
|
|
||||||
(
|
|
||||||
model_state_not_specified,
|
|
||||||
model_state_not_specified,
|
|
||||||
'"not specified" to "not specified"',
|
|
||||||
),
|
|
||||||
(model_state_not_specified, model_state_none, '"not specified" to "None"'),
|
|
||||||
(
|
|
||||||
model_state_not_specified,
|
|
||||||
model_state_empty,
|
|
||||||
'"not specified" to "empty"',
|
|
||||||
),
|
|
||||||
(model_state_none, model_state_not_specified, '"None" to "not specified"'),
|
|
||||||
(model_state_none, model_state_none, '"None" to "None"'),
|
|
||||||
(model_state_none, model_state_empty, '"None" to "empty"'),
|
|
||||||
(
|
|
||||||
model_state_empty,
|
|
||||||
model_state_not_specified,
|
|
||||||
'"empty" to "not specified"',
|
|
||||||
),
|
|
||||||
(model_state_empty, model_state_none, '"empty" to "None"'),
|
|
||||||
(model_state_empty, model_state_empty, '"empty" to "empty"'),
|
|
||||||
)
|
|
||||||
|
|
||||||
for t in tests:
|
|
||||||
test(*t)
|
|
||||||
|
|
||||||
def test_rename_index_together_to_index(self):
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book_indexes],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(changes, "otherapp", 0, ["RenameIndex"])
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
model_name="book",
|
|
||||||
new_name="book_title_author_idx",
|
|
||||||
old_fields=("author", "title"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_rename_index_together_to_index_extra_options(self):
|
|
||||||
# Indexes with extra options don't match indexes in index_together.
|
|
||||||
book_partial_index = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"indexes": [
|
|
||||||
models.Index(
|
|
||||||
fields=["author", "title"],
|
|
||||||
condition=models.Q(title__startswith="The"),
|
|
||||||
name="book_title_author_idx",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, book_partial_index],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether", "AddIndex"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_rename_index_together_to_index_order_fields(self):
|
|
||||||
# Indexes with reordered fields don't match indexes in index_together.
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book_unordered_indexes],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether", "AddIndex"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_index_together(self):
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"])
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes, "otherapp", 0, 0, name="book", index_together={("author", "title")}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_remove_index_together(self):
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"])
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes, "otherapp", 0, 0, name="book", index_together=set()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_index_together_remove_fk(self):
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book_with_no_author],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether", "RemoveField"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes, "otherapp", 0, 0, name="book", index_together=set()
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes, "otherapp", 0, 1, model_name="book", name="author"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_index_together_no_changes(self):
|
|
||||||
"""
|
|
||||||
index_together doesn't generate a migration if no changes have been
|
|
||||||
made.
|
|
||||||
"""
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
)
|
|
||||||
self.assertEqual(len(changes), 0)
|
|
||||||
|
|
||||||
def test_index_together_ordering(self):
|
|
||||||
"""index_together triggers on ordering changes."""
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together_2],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="book",
|
|
||||||
index_together={("title", "author")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_field_and_index_together(self):
|
|
||||||
"""
|
|
||||||
Added fields will be created before using them in index_together.
|
|
||||||
"""
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, AutodetectorTests.book],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together_3],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AddField", "AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
name="book",
|
|
||||||
index_together={("title", "newfield")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_model_and_index_together(self):
|
|
||||||
author = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
book_with_author = ModelState(
|
|
||||||
"otherapp",
|
|
||||||
"Book",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("author", models.ForeignKey("otherapp.Author", models.CASCADE)),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("title", "author")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.book_with_no_author], [author, book_with_author]
|
|
||||||
)
|
|
||||||
self.assertEqual(len(changes["otherapp"]), 1)
|
|
||||||
migration = changes["otherapp"][0]
|
|
||||||
self.assertEqual(len(migration.operations), 3)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["CreateModel", "AddField", "AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_remove_field_and_index_together(self):
|
|
||||||
"""
|
|
||||||
Removed fields will be removed after updating index_together.
|
|
||||||
"""
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together_3],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether", "RemoveField"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="book",
|
|
||||||
index_together={("author", "title")},
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
model_name="book",
|
|
||||||
name="newfield",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_alter_field_and_index_together(self):
|
|
||||||
"""Fields are altered after deleting some index_together."""
|
|
||||||
initial_author = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("age", models.IntegerField(db_index=True)),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("name",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
author_reversed_constraints = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200, unique=True)),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("age",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes([initial_author], [author_reversed_constraints])
|
|
||||||
|
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
[
|
|
||||||
"AlterIndexTogether",
|
|
||||||
"AlterField",
|
|
||||||
"AlterField",
|
|
||||||
"AlterIndexTogether",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="author",
|
|
||||||
index_together=set(),
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
model_name="author",
|
|
||||||
name="age",
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
model_name="author",
|
|
||||||
name="name",
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
name="author",
|
|
||||||
index_together={("age",)},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_partly_alter_index_together_increase(self):
|
|
||||||
initial_author = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("name",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
author_new_constraints = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("name",), ("age",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes([initial_author], [author_new_constraints])
|
|
||||||
|
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="author",
|
|
||||||
index_together={("name",), ("age",)},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_partly_alter_index_together_decrease(self):
|
|
||||||
initial_author = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("name",), ("age",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
author_new_constraints = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"index_together": {("age",)},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes([initial_author], [author_new_constraints])
|
|
||||||
|
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
["AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="author",
|
|
||||||
index_together={("age",)},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_rename_field_and_index_together(self):
|
|
||||||
"""Fields are renamed before updating index_together."""
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together_3],
|
|
||||||
[AutodetectorTests.author_empty, self.book_index_together_4],
|
|
||||||
MigrationQuestioner({"ask_rename": True}),
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "otherapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
["RenameField", "AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"otherapp",
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
name="book",
|
|
||||||
index_together={("title", "newfield2")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_model_order_with_respect_to_index_together(self):
|
|
||||||
changes = self.get_changes(
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
AutodetectorTests.book,
|
|
||||||
ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"order_with_respect_to": "book",
|
|
||||||
"index_together": {("name", "_order")},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
|
||||||
self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"])
|
|
||||||
self.assertOperationAttributes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
name="Author",
|
|
||||||
options={
|
|
||||||
"order_with_respect_to": "book",
|
|
||||||
"index_together": {("name", "_order")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_set_alter_order_with_respect_to_index_together(self):
|
|
||||||
after = ModelState(
|
|
||||||
"testapp",
|
|
||||||
"Author",
|
|
||||||
[
|
|
||||||
("id", models.AutoField(primary_key=True)),
|
|
||||||
("name", models.CharField(max_length=200)),
|
|
||||||
("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"order_with_respect_to": "book",
|
|
||||||
"index_together": {("name", "_order")},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
changes = self.get_changes(
|
|
||||||
[AutodetectorTests.book, AutodetectorTests.author_with_book],
|
|
||||||
[AutodetectorTests.book, after],
|
|
||||||
)
|
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
|
||||||
self.assertOperationTypes(
|
|
||||||
changes,
|
|
||||||
"testapp",
|
|
||||||
0,
|
|
||||||
["AlterOrderWithRespectTo", "AlterIndexTogether"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationSuggestNameTests(SimpleTestCase):
|
class MigrationSuggestNameTests(SimpleTestCase):
|
||||||
def test_no_operations(self):
|
def test_no_operations(self):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
|
@ -269,7 +269,6 @@ class OperationTestBase(MigrationTestBase):
|
||||||
unique_together=False,
|
unique_together=False,
|
||||||
options=False,
|
options=False,
|
||||||
db_table=None,
|
db_table=None,
|
||||||
index_together=False, # RemovedInDjango51Warning.
|
|
||||||
constraints=None,
|
constraints=None,
|
||||||
indexes=None,
|
indexes=None,
|
||||||
):
|
):
|
||||||
|
@ -277,8 +276,6 @@ class OperationTestBase(MigrationTestBase):
|
||||||
# Make the "current" state.
|
# Make the "current" state.
|
||||||
model_options = {
|
model_options = {
|
||||||
"swappable": "TEST_SWAP_MODEL",
|
"swappable": "TEST_SWAP_MODEL",
|
||||||
# RemovedInDjango51Warning.
|
|
||||||
"index_together": [["weight", "pink"]] if index_together else [],
|
|
||||||
"unique_together": [["pink", "weight"]] if unique_together else [],
|
"unique_together": [["pink", "weight"]] if unique_together else [],
|
||||||
}
|
}
|
||||||
if options:
|
if options:
|
||||||
|
|
|
@ -11,13 +11,11 @@ from django.db.models.functions import Abs, Pi
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.test import (
|
from django.test import (
|
||||||
SimpleTestCase,
|
SimpleTestCase,
|
||||||
ignore_warnings,
|
|
||||||
override_settings,
|
override_settings,
|
||||||
skipIfDBFeature,
|
skipIfDBFeature,
|
||||||
skipUnlessDBFeature,
|
skipUnlessDBFeature,
|
||||||
)
|
)
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
from .models import FoodManager, FoodQuerySet, UnicodeModel
|
from .models import FoodManager, FoodQuerySet, UnicodeModel
|
||||||
from .test_base import OperationTestBase
|
from .test_base import OperationTestBase
|
||||||
|
@ -3094,37 +3092,6 @@ class OperationTests(OperationTestBase):
|
||||||
self.assertColumnExists("test_rnflut_pony", "pink")
|
self.assertColumnExists("test_rnflut_pony", "pink")
|
||||||
self.assertColumnNotExists("test_rnflut_pony", "blue")
|
self.assertColumnNotExists("test_rnflut_pony", "blue")
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_rename_field_index_together(self):
|
|
||||||
project_state = self.set_up_test_model("test_rnflit", index_together=True)
|
|
||||||
operation = migrations.RenameField("Pony", "pink", "blue")
|
|
||||||
new_state = project_state.clone()
|
|
||||||
operation.state_forwards("test_rnflit", new_state)
|
|
||||||
self.assertIn("blue", new_state.models["test_rnflit", "pony"].fields)
|
|
||||||
self.assertNotIn("pink", new_state.models["test_rnflit", "pony"].fields)
|
|
||||||
# index_together has the renamed column.
|
|
||||||
self.assertIn(
|
|
||||||
"blue", new_state.models["test_rnflit", "pony"].options["index_together"][0]
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
"pink", new_state.models["test_rnflit", "pony"].options["index_together"][0]
|
|
||||||
)
|
|
||||||
# Rename field.
|
|
||||||
self.assertColumnExists("test_rnflit_pony", "pink")
|
|
||||||
self.assertColumnNotExists("test_rnflit_pony", "blue")
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards("test_rnflit", editor, project_state, new_state)
|
|
||||||
self.assertColumnExists("test_rnflit_pony", "blue")
|
|
||||||
self.assertColumnNotExists("test_rnflit_pony", "pink")
|
|
||||||
# The index constraint has been ported over.
|
|
||||||
self.assertIndexExists("test_rnflit_pony", ["weight", "blue"])
|
|
||||||
# Reversal.
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_backwards(
|
|
||||||
"test_rnflit", editor, new_state, project_state
|
|
||||||
)
|
|
||||||
self.assertIndexExists("test_rnflit_pony", ["weight", "pink"])
|
|
||||||
|
|
||||||
def test_rename_field_with_db_column(self):
|
def test_rename_field_with_db_column(self):
|
||||||
project_state = self.apply_operations(
|
project_state = self.apply_operations(
|
||||||
"test_rfwdbc",
|
"test_rfwdbc",
|
||||||
|
@ -3601,52 +3568,6 @@ class OperationTests(OperationTestBase):
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
migrations.RenameIndex("Pony", new_name="new_idx_name")
|
migrations.RenameIndex("Pony", new_name="new_idx_name")
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_rename_index_unnamed_index(self):
|
|
||||||
app_label = "test_rninui"
|
|
||||||
project_state = self.set_up_test_model(app_label, index_together=True)
|
|
||||||
table_name = app_label + "_pony"
|
|
||||||
self.assertIndexNameNotExists(table_name, "new_pony_test_idx")
|
|
||||||
operation = migrations.RenameIndex(
|
|
||||||
"Pony", new_name="new_pony_test_idx", old_fields=("weight", "pink")
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
operation.describe(),
|
|
||||||
"Rename unnamed index for ('weight', 'pink') on Pony to new_pony_test_idx",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
operation.migration_name_fragment,
|
|
||||||
"rename_pony_weight_pink_new_pony_test_idx",
|
|
||||||
)
|
|
||||||
|
|
||||||
new_state = project_state.clone()
|
|
||||||
operation.state_forwards(app_label, new_state)
|
|
||||||
# Rename index.
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards(app_label, editor, project_state, new_state)
|
|
||||||
self.assertIndexNameExists(table_name, "new_pony_test_idx")
|
|
||||||
# Reverse is a no-op.
|
|
||||||
with connection.schema_editor() as editor, self.assertNumQueries(0):
|
|
||||||
operation.database_backwards(app_label, editor, new_state, project_state)
|
|
||||||
self.assertIndexNameExists(table_name, "new_pony_test_idx")
|
|
||||||
# Reapply, RenameIndex operation is a noop when the old and new name
|
|
||||||
# match.
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards(app_label, editor, new_state, project_state)
|
|
||||||
self.assertIndexNameExists(table_name, "new_pony_test_idx")
|
|
||||||
# Deconstruction.
|
|
||||||
definition = operation.deconstruct()
|
|
||||||
self.assertEqual(definition[0], "RenameIndex")
|
|
||||||
self.assertEqual(definition[1], [])
|
|
||||||
self.assertEqual(
|
|
||||||
definition[2],
|
|
||||||
{
|
|
||||||
"model_name": "Pony",
|
|
||||||
"new_name": "new_pony_test_idx",
|
|
||||||
"old_fields": ("weight", "pink"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_rename_index_unknown_unnamed_index(self):
|
def test_rename_index_unknown_unnamed_index(self):
|
||||||
app_label = "test_rninuui"
|
app_label = "test_rninuui"
|
||||||
project_state = self.set_up_test_model(app_label)
|
project_state = self.set_up_test_model(app_label)
|
||||||
|
@ -3697,22 +3618,6 @@ class OperationTests(OperationTestBase):
|
||||||
self.assertIsNot(old_model, new_model)
|
self.assertIsNot(old_model, new_model)
|
||||||
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
|
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_rename_index_state_forwards_unnamed_index(self):
|
|
||||||
app_label = "test_rnidsfui"
|
|
||||||
project_state = self.set_up_test_model(app_label, index_together=True)
|
|
||||||
old_model = project_state.apps.get_model(app_label, "Pony")
|
|
||||||
new_state = project_state.clone()
|
|
||||||
|
|
||||||
operation = migrations.RenameIndex(
|
|
||||||
"Pony", new_name="new_pony_pink_idx", old_fields=("weight", "pink")
|
|
||||||
)
|
|
||||||
operation.state_forwards(app_label, new_state)
|
|
||||||
new_model = new_state.apps.get_model(app_label, "Pony")
|
|
||||||
self.assertIsNot(old_model, new_model)
|
|
||||||
self.assertEqual(new_model._meta.index_together, tuple())
|
|
||||||
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
|
|
||||||
|
|
||||||
@skipUnlessDBFeature("supports_expression_indexes")
|
@skipUnlessDBFeature("supports_expression_indexes")
|
||||||
def test_add_func_index(self):
|
def test_add_func_index(self):
|
||||||
app_label = "test_addfuncin"
|
app_label = "test_addfuncin"
|
||||||
|
@ -3832,89 +3737,12 @@ class OperationTests(OperationTestBase):
|
||||||
# Ensure the index is still there
|
# Ensure the index is still there
|
||||||
self.assertIndexExists("test_alflin_pony", ["pink"])
|
self.assertIndexExists("test_alflin_pony", ["pink"])
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_alter_index_together(self):
|
|
||||||
"""
|
|
||||||
Tests the AlterIndexTogether operation.
|
|
||||||
"""
|
|
||||||
project_state = self.set_up_test_model("test_alinto")
|
|
||||||
# Test the state alteration
|
|
||||||
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
|
|
||||||
self.assertEqual(
|
|
||||||
operation.describe(), "Alter index_together for Pony (1 constraint(s))"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
operation.migration_name_fragment,
|
|
||||||
"alter_pony_index_together",
|
|
||||||
)
|
|
||||||
new_state = project_state.clone()
|
|
||||||
operation.state_forwards("test_alinto", new_state)
|
|
||||||
self.assertEqual(
|
|
||||||
len(
|
|
||||||
project_state.models["test_alinto", "pony"].options.get(
|
|
||||||
"index_together", set()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
len(
|
|
||||||
new_state.models["test_alinto", "pony"].options.get(
|
|
||||||
"index_together", set()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
# Make sure there's no matching index
|
|
||||||
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
|
|
||||||
# Test the database alteration
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards("test_alinto", editor, project_state, new_state)
|
|
||||||
self.assertIndexExists("test_alinto_pony", ["pink", "weight"])
|
|
||||||
# And test reversal
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_backwards(
|
|
||||||
"test_alinto", editor, new_state, project_state
|
|
||||||
)
|
|
||||||
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
|
|
||||||
# And deconstruction
|
|
||||||
definition = operation.deconstruct()
|
|
||||||
self.assertEqual(definition[0], "AlterIndexTogether")
|
|
||||||
self.assertEqual(definition[1], [])
|
|
||||||
self.assertEqual(
|
|
||||||
definition[2], {"name": "Pony", "index_together": {("pink", "weight")}}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_alter_index_together_remove(self):
|
def test_alter_index_together_remove(self):
|
||||||
operation = migrations.AlterIndexTogether("Pony", None)
|
operation = migrations.AlterIndexTogether("Pony", None)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
operation.describe(), "Alter index_together for Pony (0 constraint(s))"
|
operation.describe(), "Alter index_together for Pony (0 constraint(s))"
|
||||||
)
|
)
|
||||||
|
|
||||||
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_alter_index_together_remove_with_unique_together(self):
|
|
||||||
app_label = "test_alintoremove_wunto"
|
|
||||||
table_name = "%s_pony" % app_label
|
|
||||||
project_state = self.set_up_test_model(app_label, unique_together=True)
|
|
||||||
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
|
|
||||||
# Add index together.
|
|
||||||
new_state = project_state.clone()
|
|
||||||
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
|
|
||||||
operation.state_forwards(app_label, new_state)
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards(app_label, editor, project_state, new_state)
|
|
||||||
self.assertIndexExists(table_name, ["pink", "weight"])
|
|
||||||
# Remove index together.
|
|
||||||
project_state = new_state
|
|
||||||
new_state = project_state.clone()
|
|
||||||
operation = migrations.AlterIndexTogether("Pony", set())
|
|
||||||
operation.state_forwards(app_label, new_state)
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
operation.database_forwards(app_label, editor, project_state, new_state)
|
|
||||||
self.assertIndexNotExists(table_name, ["pink", "weight"])
|
|
||||||
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
|
|
||||||
|
|
||||||
def test_add_constraint(self):
|
def test_add_constraint(self):
|
||||||
project_state = self.set_up_test_model("test_addconstraint")
|
project_state = self.set_up_test_model("test_addconstraint")
|
||||||
gt_check = models.Q(pink__gt=2)
|
gt_check = models.Q(pink__gt=2)
|
||||||
|
|
|
@ -1293,81 +1293,6 @@ class OptimizerTests(SimpleTestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_model_remove_index_together_rename_index(self):
|
|
||||||
self.assertOptimizesTo(
|
|
||||||
[
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Pony",
|
|
||||||
fields=[
|
|
||||||
("weight", models.IntegerField()),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"index_together": [("age", "weight")],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Pony",
|
|
||||||
fields=[
|
|
||||||
("weight", models.IntegerField()),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"indexes": [
|
|
||||||
models.Index(
|
|
||||||
fields=["age", "weight"], name="idx_pony_age_weight"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_model_index_together_rename_index(self):
|
|
||||||
self.assertOptimizesTo(
|
|
||||||
[
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Pony",
|
|
||||||
fields=[
|
|
||||||
("weight", models.IntegerField()),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
("height", models.IntegerField()),
|
|
||||||
("rank", models.IntegerField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"index_together": [("age", "weight"), ("height", "rank")],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Pony",
|
|
||||||
fields=[
|
|
||||||
("weight", models.IntegerField()),
|
|
||||||
("age", models.IntegerField()),
|
|
||||||
("height", models.IntegerField()),
|
|
||||||
("rank", models.IntegerField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"index_together": {("height", "rank")},
|
|
||||||
"indexes": [
|
|
||||||
models.Index(
|
|
||||||
fields=["age", "weight"], name="idx_pony_age_weight"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_model_rename_index_no_old_fields(self):
|
def test_create_model_rename_index_no_old_fields(self):
|
||||||
self.assertOptimizesTo(
|
self.assertOptimizesTo(
|
||||||
[
|
[
|
||||||
|
|
|
@ -13,9 +13,8 @@ from django.db.migrations.state import (
|
||||||
ProjectState,
|
ProjectState,
|
||||||
get_related_models_recursive,
|
get_related_models_recursive,
|
||||||
)
|
)
|
||||||
from django.test import SimpleTestCase, ignore_warnings, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
from django.test.utils import isolate_apps
|
from django.test.utils import isolate_apps
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
FoodManager,
|
FoodManager,
|
||||||
|
@ -31,9 +30,6 @@ class StateTests(SimpleTestCase):
|
||||||
Tests state construction, rendering and modification by operations.
|
Tests state construction, rendering and modification by operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# RemovedInDjango51Warning, when deprecation ends, only remove
|
|
||||||
# Meta.index_together from inline models.
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""
|
"""
|
||||||
Tests making a ProjectState from an Apps
|
Tests making a ProjectState from an Apps
|
||||||
|
@ -50,7 +46,6 @@ class StateTests(SimpleTestCase):
|
||||||
app_label = "migrations"
|
app_label = "migrations"
|
||||||
apps = new_apps
|
apps = new_apps
|
||||||
unique_together = ["name", "bio"]
|
unique_together = ["name", "bio"]
|
||||||
index_together = ["bio", "age"] # RemovedInDjango51Warning.
|
|
||||||
|
|
||||||
class AuthorProxy(Author):
|
class AuthorProxy(Author):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -142,7 +137,6 @@ class StateTests(SimpleTestCase):
|
||||||
author_state.options,
|
author_state.options,
|
||||||
{
|
{
|
||||||
"unique_together": {("name", "bio")},
|
"unique_together": {("name", "bio")},
|
||||||
"index_together": {("bio", "age")}, # RemovedInDjango51Warning.
|
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"constraints": [],
|
"constraints": [],
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,14 +54,8 @@ from django.db.models.fields.json import KeyTextTransform
|
||||||
from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Upper
|
from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Upper
|
||||||
from django.db.models.indexes import IndexExpression
|
from django.db.models.indexes import IndexExpression
|
||||||
from django.db.transaction import TransactionManagementError, atomic
|
from django.db.transaction import TransactionManagementError, atomic
|
||||||
from django.test import (
|
from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||||
TransactionTestCase,
|
|
||||||
ignore_warnings,
|
|
||||||
skipIfDBFeature,
|
|
||||||
skipUnlessDBFeature,
|
|
||||||
)
|
|
||||||
from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
|
from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField
|
from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -3351,7 +3345,6 @@ class SchemaTests(TransactionTestCase):
|
||||||
self.assertIsNone(editor.add_constraint(Author, constraint))
|
self.assertIsNone(editor.add_constraint(Author, constraint))
|
||||||
self.assertIsNone(editor.remove_constraint(Author, constraint))
|
self.assertIsNone(editor.remove_constraint(Author, constraint))
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_index_together(self):
|
def test_index_together(self):
|
||||||
"""
|
"""
|
||||||
Tests removing and adding index_together constraints on a model.
|
Tests removing and adding index_together constraints on a model.
|
||||||
|
@ -3395,128 +3388,6 @@ class SchemaTests(TransactionTestCase):
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
def test_index_together_with_fk(self):
|
|
||||||
"""
|
|
||||||
Tests removing and adding index_together constraints that include
|
|
||||||
a foreign key.
|
|
||||||
"""
|
|
||||||
# Create the table
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.create_model(Author)
|
|
||||||
editor.create_model(Book)
|
|
||||||
# Ensure the fields are unique to begin with
|
|
||||||
self.assertEqual(Book._meta.index_together, ())
|
|
||||||
# Add the unique_together constraint
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.alter_index_together(Book, [], [["author", "title"]])
|
|
||||||
# Alter it back
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.alter_index_together(Book, [["author", "title"]], [])
|
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
@isolate_apps("schema")
|
|
||||||
def test_create_index_together(self):
|
|
||||||
"""
|
|
||||||
Tests creating models with index_together already defined
|
|
||||||
"""
|
|
||||||
|
|
||||||
class TagIndexed(Model):
|
|
||||||
title = CharField(max_length=255)
|
|
||||||
slug = SlugField(unique=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = "schema"
|
|
||||||
index_together = [["slug", "title"]]
|
|
||||||
|
|
||||||
# Create the table
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.create_model(TagIndexed)
|
|
||||||
self.isolated_local_models = [TagIndexed]
|
|
||||||
# Ensure there is an index
|
|
||||||
self.assertIs(
|
|
||||||
any(
|
|
||||||
c["index"]
|
|
||||||
for c in self.get_constraints("schema_tagindexed").values()
|
|
||||||
if c["columns"] == ["slug", "title"]
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
|
|
||||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
|
||||||
@isolate_apps("schema")
|
|
||||||
def test_remove_index_together_does_not_remove_meta_indexes(self):
|
|
||||||
class AuthorWithIndexedNameAndBirthday(Model):
|
|
||||||
name = CharField(max_length=255)
|
|
||||||
birthday = DateField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = "schema"
|
|
||||||
index_together = [["name", "birthday"]]
|
|
||||||
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.create_model(AuthorWithIndexedNameAndBirthday)
|
|
||||||
self.isolated_local_models = [AuthorWithIndexedNameAndBirthday]
|
|
||||||
# Add the custom index
|
|
||||||
index = Index(fields=["name", "birthday"], name="author_name_birthday_idx")
|
|
||||||
custom_index_name = index.name
|
|
||||||
AuthorWithIndexedNameAndBirthday._meta.indexes = [index]
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.add_index(AuthorWithIndexedNameAndBirthday, index)
|
|
||||||
# Ensure the indexes exist
|
|
||||||
constraints = self.get_constraints(
|
|
||||||
AuthorWithIndexedNameAndBirthday._meta.db_table
|
|
||||||
)
|
|
||||||
self.assertIn(custom_index_name, constraints)
|
|
||||||
other_constraints = [
|
|
||||||
name
|
|
||||||
for name, details in constraints.items()
|
|
||||||
if details["columns"] == ["name", "birthday"]
|
|
||||||
and details["index"]
|
|
||||||
and name != custom_index_name
|
|
||||||
]
|
|
||||||
self.assertEqual(len(other_constraints), 1)
|
|
||||||
# Remove index together
|
|
||||||
index_together = AuthorWithIndexedNameAndBirthday._meta.index_together
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.alter_index_together(
|
|
||||||
AuthorWithIndexedNameAndBirthday, index_together, []
|
|
||||||
)
|
|
||||||
constraints = self.get_constraints(
|
|
||||||
AuthorWithIndexedNameAndBirthday._meta.db_table
|
|
||||||
)
|
|
||||||
self.assertIn(custom_index_name, constraints)
|
|
||||||
other_constraints = [
|
|
||||||
name
|
|
||||||
for name, details in constraints.items()
|
|
||||||
if details["columns"] == ["name", "birthday"]
|
|
||||||
and details["index"]
|
|
||||||
and name != custom_index_name
|
|
||||||
]
|
|
||||||
self.assertEqual(len(other_constraints), 0)
|
|
||||||
# Re-add index together
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
editor.alter_index_together(
|
|
||||||
AuthorWithIndexedNameAndBirthday, [], index_together
|
|
||||||
)
|
|
||||||
constraints = self.get_constraints(
|
|
||||||
AuthorWithIndexedNameAndBirthday._meta.db_table
|
|
||||||
)
|
|
||||||
self.assertIn(custom_index_name, constraints)
|
|
||||||
other_constraints = [
|
|
||||||
name
|
|
||||||
for name, details in constraints.items()
|
|
||||||
if details["columns"] == ["name", "birthday"]
|
|
||||||
and details["index"]
|
|
||||||
and name != custom_index_name
|
|
||||||
]
|
|
||||||
self.assertEqual(len(other_constraints), 1)
|
|
||||||
# Drop the index
|
|
||||||
with connection.schema_editor() as editor:
|
|
||||||
AuthorWithIndexedNameAndBirthday._meta.indexes = []
|
|
||||||
editor.remove_index(AuthorWithIndexedNameAndBirthday, index)
|
|
||||||
|
|
||||||
@isolate_apps("schema")
|
@isolate_apps("schema")
|
||||||
def test_db_table(self):
|
def test_db_table(self):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue