summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorAkshesh <aksheshdoshi@gmail.com>2016-06-25 22:02:56 +0530
committerTim Graham <timograham@gmail.com>2016-06-27 10:41:01 -0400
commit156e2d59cf92eb252c2f6ef9bb0b65f26c707de2 (patch)
treea385db97c67f6b8504954c3df3ac88f169d13f8c /django
parentc962b9104a22505a7d6c57bbec9eda163f486c7d (diff)
Fixed #26709 -- Added class-based indexes.
Added the AddIndex and RemoveIndex operations to use them in migrations. Thanks markush, mjtamlyn, timgraham, and charettes for review and advice.
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/base/schema.py27
-rw-r--r--django/db/migrations/operations/__init__.py10
-rw-r--r--django/db/migrations/operations/models.py77
-rw-r--r--django/db/migrations/state.py7
-rw-r--r--django/db/models/__init__.py1
-rw-r--r--django/db/models/indexes.py113
-rw-r--r--django/db/models/options.py2
7 files changed, 225 insertions, 12 deletions
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 156f119c0d..b6510ca626 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -316,6 +316,18 @@ class BaseDatabaseSchemaEditor(object):
"table": self.quote_name(model._meta.db_table),
})
+ def add_index(self, index):
+ """
+ Add an index on a model.
+ """
+ self.execute(index.create_sql(self))
+
+ def remove_index(self, index):
+ """
+ Remove an index from a model.
+ """
+ self.execute(index.remove_sql(self))
+
def alter_unique_together(self, model, old_unique_together, new_unique_together):
"""
Deals with a model changing its unique_together.
@@ -836,12 +848,7 @@ class BaseDatabaseSchemaEditor(object):
index_name = "D%s" % index_name[:-1]
return index_name
- def _create_index_sql(self, model, fields, suffix="", sql=None):
- """
- Return the SQL statement to create the index for one or several fields.
- `sql` can be specified if the syntax differs from the standard (GIS
- indexes, ...).
- """
+ def _get_index_tablespace_sql(self, model, fields):
if len(fields) == 1 and fields[0].db_tablespace:
tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
elif model._meta.db_tablespace:
@@ -850,7 +857,15 @@ class BaseDatabaseSchemaEditor(object):
tablespace_sql = ""
if tablespace_sql:
tablespace_sql = " " + tablespace_sql
+ return tablespace_sql
+ def _create_index_sql(self, model, fields, suffix="", sql=None):
+ """
+ Return the SQL statement to create the index for one or several fields.
+ `sql` can be specified if the syntax differs from the standard (GIS
+ indexes, ...).
+ """
+ tablespace_sql = self._get_index_tablespace_sql(model, fields)
columns = [field.column for field in fields]
sql_create_index = sql or self.sql_create_index
return sql_create_index % {
diff --git a/django/db/migrations/operations/__init__.py b/django/db/migrations/operations/__init__.py
index 472b482eff..894f2ab9c5 100644
--- a/django/db/migrations/operations/__init__.py
+++ b/django/db/migrations/operations/__init__.py
@@ -1,15 +1,15 @@
from .fields import AddField, AlterField, RemoveField, RenameField
from .models import (
- AlterIndexTogether, AlterModelManagers, AlterModelOptions, AlterModelTable,
- AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel, DeleteModel,
- RenameModel,
+ AddIndex, AlterIndexTogether, AlterModelManagers, AlterModelOptions,
+ AlterModelTable, AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel,
+ DeleteModel, RemoveIndex, RenameModel,
)
from .special import RunPython, RunSQL, SeparateDatabaseAndState
__all__ = [
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
- 'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
- 'AddField', 'RemoveField', 'AlterField', 'RenameField',
+ 'RenameModel', 'AlterIndexTogether', 'AlterModelOptions', 'AddIndex',
+ 'RemoveIndex', 'AddField', 'RemoveField', 'AlterField', 'RenameField',
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
'AlterOrderWithRespectTo', 'AlterModelManagers',
]
diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py
index b7d4046f87..4c9467cc0d 100644
--- a/django/db/migrations/operations/models.py
+++ b/django/db/migrations/operations/models.py
@@ -742,3 +742,80 @@ class AlterModelManagers(ModelOptionOperation):
def describe(self):
return "Change managers on %s" % (self.name, )
+
+
+class AddIndex(Operation):
+ """
+ Add an index on a model.
+ """
+
+ def __init__(self, model_name, index):
+ self.model_name = model_name
+ self.index = index
+
+ def state_forwards(self, app_label, state):
+ model_state = state.models[app_label, self.model_name.lower()]
+ self.index.model = state.apps.get_model(app_label, self.model_name)
+ model_state.options['indexes'].append(self.index)
+
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ schema_editor.add_index(self.index)
+
+ def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ schema_editor.remove_index(self.index)
+
+ def deconstruct(self):
+ kwargs = {
+ 'model_name': self.model_name,
+ 'index': self.index,
+ }
+ return (
+ self.__class__.__name__,
+ [],
+ kwargs,
+ )
+
+ def describe(self):
+ return 'Create index on field(s) %s of model %s' % (
+ ', '.join(self.index.fields),
+ self.model_name,
+ )
+
+
+class RemoveIndex(Operation):
+ """
+ Remove an index from a model.
+ """
+
+ def __init__(self, model_name, name):
+ self.model_name = model_name
+ self.name = name
+
+ def state_forwards(self, app_label, state):
+ model_state = state.models[app_label, self.model_name.lower()]
+ indexes = model_state.options['indexes']
+ model_state.options['indexes'] = [idx for idx in indexes if idx.name != self.name]
+
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ from_model_state = from_state.models[app_label, self.model_name.lower()]
+ index = from_model_state.get_index_by_name(self.name)
+ schema_editor.remove_index(index)
+
+ def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ to_model_state = to_state.models[app_label, self.model_name.lower()]
+ index = to_model_state.get_index_by_name(self.name)
+ schema_editor.add_index(index)
+
+ def deconstruct(self):
+ kwargs = {
+ 'model_name': self.model_name,
+ 'name': self.name,
+ }
+ return (
+ self.__class__.__name__,
+ [],
+ kwargs,
+ )
+
+ def describe(self):
+ return 'Remove index %s from %s' % (self.name, self.model_name)
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index 44eab227ed..74f7475cf8 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -330,6 +330,7 @@ class ModelState(object):
self.name = force_text(name)
self.fields = fields
self.options = options or {}
+ self.options.setdefault('indexes', [])
self.bases = bases or (models.Model, )
self.managers = managers or []
# Sanity-check that fields is NOT a dict. It must be ordered.
@@ -557,6 +558,12 @@ class ModelState(object):
return field
raise ValueError("No field called %s on model %s" % (name, self.name))
+ def get_index_by_name(self, name):
+ for index in self.options['indexes']:
+ if index.name == name:
+ return index
+ raise ValueError("No index named %s on model %s" % (name, self.name))
+
def __repr__(self):
return "<ModelState: '%s.%s'>" % (self.app_label, self.name)
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 958b122d9f..2ce935d3f8 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -12,6 +12,7 @@ from django.db.models.expressions import ( # NOQA
from django.db.models.fields import * # NOQA
from django.db.models.fields.files import FileField, ImageField # NOQA
from django.db.models.fields.proxy import OrderWrt # NOQA
+from django.db.models.indexes import * # NOQA
from django.db.models.lookups import Lookup, Transform # NOQA
from django.db.models.manager import Manager # NOQA
from django.db.models.query import ( # NOQA
diff --git a/django/db/models/indexes.py b/django/db/models/indexes.py
new file mode 100644
index 0000000000..15d618156e
--- /dev/null
+++ b/django/db/models/indexes.py
@@ -0,0 +1,113 @@
+from __future__ import unicode_literals
+
+import hashlib
+
+from django.utils.encoding import force_bytes
+from django.utils.functional import cached_property
+
+__all__ = ['Index']
+
+# The max length of the names of the indexes (restricted to 30 due to Oracle)
+MAX_NAME_LENGTH = 30
+
+
+class Index(object):
+ suffix = 'idx'
+
+ def __init__(self, fields=[], name=None):
+ if not fields:
+ raise ValueError('At least one field is required to define an index.')
+ self.fields = fields
+ self._name = name or ''
+ if self._name:
+ errors = self.check_name()
+ if len(self._name) > MAX_NAME_LENGTH:
+ errors.append('Index names cannot be longer than %s characters.' % MAX_NAME_LENGTH)
+ if errors:
+ raise ValueError(errors)
+
+ @cached_property
+ def name(self):
+ if not self._name:
+ self._name = self.get_name()
+ self.check_name()
+ return self._name
+
+ def check_name(self):
+ errors = []
+ # Name can't start with an underscore on Oracle; prepend D if needed.
+ if self._name[0] == '_':
+ errors.append('Index names cannot start with an underscore (_).')
+ self._name = 'D%s' % self._name[1:]
+ # Name can't start with a number on Oracle; prepend D if needed.
+ elif self._name[0].isdigit():
+ errors.append('Index names cannot start with a number (0-9).')
+ self._name = 'D%s' % self._name[1:]
+ return errors
+
+ def create_sql(self, schema_editor):
+ fields = [self.model._meta.get_field(field) for field in self.fields]
+ tablespace_sql = schema_editor._get_index_tablespace_sql(self.model, fields)
+ columns = [field.column for field in fields]
+
+ quote_name = schema_editor.quote_name
+ return schema_editor.sql_create_index % {
+ 'table': quote_name(self.model._meta.db_table),
+ 'name': quote_name(self.name),
+ 'columns': ', '.join(quote_name(column) for column in columns),
+ 'extra': tablespace_sql,
+ }
+
+ def remove_sql(self, schema_editor):
+ quote_name = schema_editor.quote_name
+ return schema_editor.sql_delete_index % {
+ 'table': quote_name(self.model._meta.db_table),
+ 'name': quote_name(self.name),
+ }
+
+ def deconstruct(self):
+ path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
+ path = path.replace('django.db.models.indexes', 'django.db.models')
+ return (path, (), {'fields': self.fields})
+
+ @staticmethod
+ def _hash_generator(*args):
+ """
+ Generate a 32-bit digest of a set of arguments that can be used to
+ shorten identifying names.
+ """
+ h = hashlib.md5()
+ for arg in args:
+ h.update(force_bytes(arg))
+ return h.hexdigest()[:6]
+
+ def get_name(self):
+ """
+ Generate a unique name for the index.
+
+ The name is divided into 3 parts - table name (12 chars), field name
+ (8 chars) and unique hash + suffix (10 chars). Each part is made to
+ fit its size by truncating the excess length.
+ """
+ table_name = self.model._meta.db_table
+ column_names = [self.model._meta.get_field(field).column for field in self.fields]
+ hash_data = [table_name] + column_names + [self.suffix]
+ index_name = '%s_%s_%s' % (
+ table_name[:11],
+ column_names[0][:7],
+ '%s_%s' % (self._hash_generator(*hash_data), self.suffix),
+ )
+ assert len(index_name) <= 30, (
+ 'Index too long for multiple database support. Is self.suffix '
+ 'longer than 3 characters?'
+ )
+ return index_name
+
+ def __repr__(self):
+ return "<%s: fields='%s'>" % (self.__class__.__name__, ', '.join(self.fields))
+
+ def __eq__(self, other):
+ return (self.__class__ == other.__class__) and (self.deconstruct() == other.deconstruct())
+
+ def __ne__(self, other):
+ return not (self == other)
diff --git a/django/db/models/options.py b/django/db/models/options.py
index ef13c496f8..19dce58663 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -43,7 +43,7 @@ DEFAULT_NAMES = (
'auto_created', 'index_together', 'apps', 'default_permissions',
'select_on_save', 'default_related_name', 'required_db_features',
'required_db_vendor', 'base_manager_name', 'default_manager_name',
- 'manager_inheritance_from_future',
+ 'manager_inheritance_from_future', 'indexes',
)