diff options
| author | Akshesh <aksheshdoshi@gmail.com> | 2016-06-25 22:02:56 +0530 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2016-06-27 10:41:01 -0400 |
| commit | 156e2d59cf92eb252c2f6ef9bb0b65f26c707de2 (patch) | |
| tree | a385db97c67f6b8504954c3df3ac88f169d13f8c /django | |
| parent | c962b9104a22505a7d6c57bbec9eda163f486c7d (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.py | 27 | ||||
| -rw-r--r-- | django/db/migrations/operations/__init__.py | 10 | ||||
| -rw-r--r-- | django/db/migrations/operations/models.py | 77 | ||||
| -rw-r--r-- | django/db/migrations/state.py | 7 | ||||
| -rw-r--r-- | django/db/models/__init__.py | 1 | ||||
| -rw-r--r-- | django/db/models/indexes.py | 113 | ||||
| -rw-r--r-- | django/db/models/options.py | 2 |
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', ) |
