summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@gmail.com>2022-05-02 17:22:54 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-05-12 20:44:03 +0200
commiteacd4977f6a4bb038e82796ba79a2f61bae330c6 (patch)
tree8d9b1c7d18226a8922826257ed997cee6229af10 /django/db
parent20e65a34aea0ace077033c84854dcf225e248f8c (diff)
Refs #27064 -- Added RenameIndex migration operation.
Diffstat (limited to 'django/db')
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/base/schema.py19
-rw-r--r--django/db/backends/mysql/features.py6
-rw-r--r--django/db/backends/mysql/schema.py1
-rw-r--r--django/db/backends/oracle/features.py1
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/migrations/operations/__init__.py2
-rw-r--r--django/db/migrations/operations/models.py146
-rw-r--r--django/db/migrations/state.py22
9 files changed, 201 insertions, 0 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index a96dbe1b08..68cad9fef2 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -176,6 +176,9 @@ class BaseDatabaseFeatures:
# Can it create foreign key constraints inline when adding columns?
can_create_inline_fk = True
+ # Can an index be renamed?
+ can_rename_index = False
+
# Does it automatically index foreign keys?
indexes_foreign_keys = True
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 468ab42490..f2ca8c8df9 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -131,6 +131,7 @@ class BaseDatabaseSchemaEditor:
"CREATE UNIQUE INDEX %(name)s ON %(table)s "
"(%(columns)s)%(include)s%(condition)s"
)
+ sql_rename_index = "ALTER INDEX %(old_name)s RENAME TO %(new_name)s"
sql_delete_index = "DROP INDEX %(name)s"
sql_create_pk = (
@@ -492,6 +493,16 @@ class BaseDatabaseSchemaEditor:
return None
self.execute(index.remove_sql(model, self))
+ def rename_index(self, model, old_index, new_index):
+ if self.connection.features.can_rename_index:
+ self.execute(
+ self._rename_index_sql(model, old_index.name, new_index.name),
+ params=None,
+ )
+ else:
+ self.remove_index(model, old_index)
+ self.add_index(model, new_index)
+
def add_constraint(self, model, constraint):
"""Add a constraint to a model."""
sql = constraint.create_sql(model, self)
@@ -1361,6 +1372,14 @@ class BaseDatabaseSchemaEditor:
name=self.quote_name(name),
)
+ def _rename_index_sql(self, model, old_name, new_name):
+ return Statement(
+ self.sql_rename_index,
+ table=Table(model._meta.db_table, self.quote_name),
+ old_name=self.quote_name(old_name),
+ new_name=self.quote_name(new_name),
+ )
+
def _index_columns(self, table, columns, col_suffixes, opclasses):
return Columns(table, columns, self.quote_name, col_suffixes=col_suffixes)
diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index 7b9a90ab06..1261d9e9a6 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -344,3 +344,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
and self._mysql_storage_engine != "MyISAM"
and self.connection.mysql_version >= (8, 0, 13)
)
+
+ @cached_property
+ def can_rename_index(self):
+ if self.connection.mysql_is_mariadb:
+ return self.connection.mysql_version >= (10, 5, 2)
+ return True
diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py
index 562b209eef..d6d303f0f0 100644
--- a/django/db/backends/mysql/schema.py
+++ b/django/db/backends/mysql/schema.py
@@ -23,6 +23,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
sql_delete_index = "DROP INDEX %(name)s ON %(table)s"
+ sql_rename_index = "ALTER TABLE %(table)s RENAME INDEX %(old_name)s TO %(new_name)s"
sql_create_pk = (
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s PRIMARY KEY (%(columns)s)"
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index e0db3daa88..defd0a0ff8 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -60,6 +60,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_ignore_conflicts = False
max_query_params = 2**16 - 1
supports_partial_indexes = False
+ can_rename_index = True
supports_slicing_ordering_in_compound = True
allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 57688642eb..ce01e88603 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -60,6 +60,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_update_conflicts = True
supports_update_conflicts_with_target = True
supports_covering_indexes = True
+ can_rename_index = True
test_collations = {
"non_default": "sv-x-icu",
"swedish_ci": "sv-x-icu",
diff --git a/django/db/migrations/operations/__init__.py b/django/db/migrations/operations/__init__.py
index 793969ed12..987c7c1fe6 100644
--- a/django/db/migrations/operations/__init__.py
+++ b/django/db/migrations/operations/__init__.py
@@ -12,6 +12,7 @@ from .models import (
DeleteModel,
RemoveConstraint,
RemoveIndex,
+ RenameIndex,
RenameModel,
)
from .special import RunPython, RunSQL, SeparateDatabaseAndState
@@ -26,6 +27,7 @@ __all__ = [
"AlterModelOptions",
"AddIndex",
"RemoveIndex",
+ "RenameIndex",
"AddField",
"RemoveField",
"AlterField",
diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py
index 8a5c04393b..d17232e4ec 100644
--- a/django/db/migrations/operations/models.py
+++ b/django/db/migrations/operations/models.py
@@ -876,6 +876,152 @@ class RemoveIndex(IndexOperation):
return "remove_%s_%s" % (self.model_name_lower, self.name.lower())
+class RenameIndex(IndexOperation):
+ """Rename an index."""
+
+ def __init__(self, model_name, new_name, old_name=None, old_fields=None):
+ if not old_name and not old_fields:
+ raise ValueError(
+ "RenameIndex requires one of old_name and old_fields arguments to be "
+ "set."
+ )
+ if old_name and old_fields:
+ raise ValueError(
+ "RenameIndex.old_name and old_fields are mutually exclusive."
+ )
+ self.model_name = model_name
+ self.new_name = new_name
+ self.old_name = old_name
+ self.old_fields = old_fields
+
+ @cached_property
+ def old_name_lower(self):
+ return self.old_name.lower()
+
+ @cached_property
+ def new_name_lower(self):
+ return self.new_name.lower()
+
+ def deconstruct(self):
+ kwargs = {
+ "model_name": self.model_name,
+ "new_name": self.new_name,
+ }
+ if self.old_name:
+ kwargs["old_name"] = self.old_name
+ if self.old_fields:
+ kwargs["old_fields"] = self.old_fields
+ return (self.__class__.__qualname__, [], kwargs)
+
+ def state_forwards(self, app_label, state):
+ if self.old_fields:
+ state.add_index(
+ app_label,
+ self.model_name_lower,
+ models.Index(fields=self.old_fields, name=self.new_name),
+ )
+ state.remove_model_options(
+ app_label,
+ self.model_name_lower,
+ AlterIndexTogether.option_name,
+ self.old_fields,
+ )
+ else:
+ state.rename_index(
+ app_label, self.model_name_lower, self.old_name, self.new_name
+ )
+
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ model = to_state.apps.get_model(app_label, self.model_name)
+ if not self.allow_migrate_model(schema_editor.connection.alias, model):
+ return
+
+ if self.old_fields:
+ from_model = from_state.apps.get_model(app_label, self.model_name)
+ columns = [
+ from_model._meta.get_field(field).column for field in self.old_fields
+ ]
+ matching_index_name = schema_editor._constraint_names(
+ from_model, column_names=columns, index=True
+ )
+ if len(matching_index_name) != 1:
+ raise ValueError(
+ "Found wrong number (%s) of indexes for %s(%s)."
+ % (
+ len(matching_index_name),
+ from_model._meta.db_table,
+ ", ".join(columns),
+ )
+ )
+ old_index = models.Index(
+ fields=self.old_fields,
+ name=matching_index_name[0],
+ )
+ else:
+ from_model_state = from_state.models[app_label, self.model_name_lower]
+ old_index = from_model_state.get_index_by_name(self.old_name)
+
+ to_model_state = to_state.models[app_label, self.model_name_lower]
+ new_index = to_model_state.get_index_by_name(self.new_name)
+ schema_editor.rename_index(model, old_index, new_index)
+
+ def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ if self.old_fields:
+ # Backward operation with unnamed index is a no-op.
+ return
+
+ self.new_name_lower, self.old_name_lower = (
+ self.old_name_lower,
+ self.new_name_lower,
+ )
+ self.new_name, self.old_name = self.old_name, self.new_name
+
+ self.database_forwards(app_label, schema_editor, from_state, to_state)
+
+ self.new_name_lower, self.old_name_lower = (
+ self.old_name_lower,
+ self.new_name_lower,
+ )
+ self.new_name, self.old_name = self.old_name, self.new_name
+
+ def describe(self):
+ if self.old_name:
+ return (
+ f"Rename index {self.old_name} on {self.model_name} to {self.new_name}"
+ )
+ return (
+ f"Rename unnamed index for {self.old_fields} on {self.model_name} to "
+ f"{self.new_name}"
+ )
+
+ @property
+ def migration_name_fragment(self):
+ if self.old_name:
+ return "rename_%s_%s" % (self.old_name_lower, self.new_name_lower)
+ return "rename_%s_%s_%s" % (
+ self.model_name_lower,
+ "_".join(self.old_fields),
+ self.new_name_lower,
+ )
+
+ def reduce(self, operation, app_label):
+ if (
+ isinstance(operation, RenameIndex)
+ and self.model_name_lower == operation.model_name_lower
+ and operation.old_name
+ and self.new_name_lower == operation.old_name_lower
+ ):
+ return [
+ RenameIndex(
+ self.model_name,
+ new_name=operation.new_name,
+ old_name=self.old_name,
+ old_fields=self.old_fields,
+ )
+ ]
+ return super().reduce(operation, app_label)
+
+
class AddConstraint(IndexOperation):
option_name = "constraints"
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index 7d7a9174f0..ff5d0e93a9 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -187,6 +187,14 @@ class ProjectState:
model_state.options.pop(key, False)
self.reload_model(app_label, model_name, delay=True)
+ def remove_model_options(self, app_label, model_name, option_name, value_to_remove):
+ model_state = self.models[app_label, model_name]
+ if objs := model_state.options.get(option_name):
+ model_state.options[option_name] = [
+ obj for obj in objs if tuple(obj) != tuple(value_to_remove)
+ ]
+ self.reload_model(app_label, model_name, delay=True)
+
def alter_model_managers(self, app_label, model_name, managers):
model_state = self.models[app_label, model_name]
model_state.managers = list(managers)
@@ -209,6 +217,20 @@ class ProjectState:
def remove_index(self, app_label, model_name, index_name):
self._remove_option(app_label, model_name, "indexes", index_name)
+ def rename_index(self, app_label, model_name, old_index_name, new_index_name):
+ model_state = self.models[app_label, model_name]
+ objs = model_state.options["indexes"]
+
+ new_indexes = []
+ for obj in objs:
+ if obj.name == old_index_name:
+ obj = obj.clone()
+ obj.name = new_index_name
+ new_indexes.append(obj)
+
+ model_state.options["indexes"] = new_indexes
+ self.reload_model(app_label, model_name, delay=True)
+
def add_constraint(self, app_label, model_name, constraint):
self._append_option(app_label, model_name, "constraints", constraint)