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 "" % (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', ) diff --git a/docs/index.txt b/docs/index.txt index 58044f574e..ab38fa02f9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -82,6 +82,7 @@ manipulating the data of your Web application. Learn more about it below: * **Models:** :doc:`Introduction to models ` | :doc:`Field types ` | + :doc:`Indexes ` | :doc:`Meta options ` | :doc:`Model class ` diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index c8668d108e..485add5bcc 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -192,6 +192,43 @@ field like ``models.IntegerField()`` on most databases. Changes a field's name (and, unless :attr:`~django.db.models.Field.db_column` is set, its column name). +``AddIndex`` +------------ + +.. class:: AddIndex(model_name, index) + +.. versionadded:: 1.11 + +Creates an index in the database table for the model with ``model_name``. +``index`` is an instance of the :class:`~django.db.models.Index` class. + +For example, to add an index on the ``title`` and ``author`` fields of the +``Book`` model:: + + from django.db import migrations, models + + class Migration(migrations.Migration): + operations = [ + migrations.AddIndex( + 'Book', + models.Index(fields=['title', 'author'], name='my_index_name'), + ), + ] + +If you're writing your own migration to add an index, it's recommended to pass +a ``name`` to the ``index`` as done above so that you can reference it if you +later want to remove it. Otherwise, a name will be autogenerated and you'll +have to inspect the database to find the index name if you want to remove it. + +``RemoveIndex`` +--------------- + +.. class:: RemoveIndex(model_name, name) + +.. versionadded:: 1.11 + +Removes the index named ``name`` from the model with ``model_name``. + Special Operations ================== diff --git a/docs/ref/models/index.txt b/docs/ref/models/index.txt index 103775e269..d731ee37dc 100644 --- a/docs/ref/models/index.txt +++ b/docs/ref/models/index.txt @@ -8,6 +8,7 @@ Model API reference. For introductory material, see :doc:`/topics/db/models`. :maxdepth: 1 fields + indexes meta relations class diff --git a/docs/ref/models/indexes.txt b/docs/ref/models/indexes.txt new file mode 100644 index 0000000000..c3c8d1b6eb --- /dev/null +++ b/docs/ref/models/indexes.txt @@ -0,0 +1,48 @@ +===================== +Model index reference +===================== + +.. module:: django.db.models.indexes + +.. currentmodule:: django.db.models + +.. versionadded:: 1.11 + +Index classes ease creating database indexes. This document explains the API +references of :class:`Index` which includes the `index options`_. + +.. admonition:: Referencing built-in indexes + + Indexes are defined in ``django.db.models.indexes``, but for convenience + they're imported into :mod:`django.db.models`. The standard convention is + to use ``from django.db import models`` and refer to the indexes as + ``models.``. + +``Index`` options +================= + +.. class:: Index(fields=[], name=None) + + Creates an index (B-Tree) in the database. + +``fields`` +----------- + +.. attribute:: Index.fields + +A list of the name of the fields on which the index is desired. + +``name`` +-------- + +.. attribute:: Index.name + +The name of the index. If ``name`` isn't provided Django will auto-generate a +name. For compatibility with different databases, index names cannot be longer +than 30 characters and shouldn't start with a number (0-9) or underscore (_). + +.. seealso:: + + Use the :class:`~django.db.migrations.operations.AddIndex` and + :class:`~django.db.migrations.operations.RemoveIndex` operations to add + and remove indexes. diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index cb20927e07..1a1f6a1250 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -1263,7 +1263,9 @@ class AutodetectorTests(TestCase): # Right number/type of migrations? self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"]) - self.assertOperationAttributes(changes, "testapp", 0, 0, name="AuthorProxy", options={"proxy": True}) + self.assertOperationAttributes( + changes, "testapp", 0, 0, name="AuthorProxy", options={"proxy": True, "indexes": []} + ) # Now, we test turning a proxy model into a non-proxy model # It should delete the proxy then make the real one changes = self.get_changes( @@ -1273,7 +1275,7 @@ class AutodetectorTests(TestCase): self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes(changes, "testapp", 0, ["DeleteModel", "CreateModel"]) self.assertOperationAttributes(changes, "testapp", 0, 0, name="AuthorProxy") - self.assertOperationAttributes(changes, "testapp", 0, 1, name="AuthorProxy", options={}) + self.assertOperationAttributes(changes, "testapp", 0, 1, name="AuthorProxy", options={"indexes": []}) def test_proxy_custom_pk(self): """ @@ -1296,7 +1298,9 @@ class AutodetectorTests(TestCase): # Right number/type of migrations? self.assertNumberMigrations(changes, 'testapp', 1) self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"]) - self.assertOperationAttributes(changes, 'testapp', 0, 0, name="AuthorUnmanaged", options={"managed": False}) + self.assertOperationAttributes( + changes, 'testapp', 0, 0, name="AuthorUnmanaged", options={"managed": False, "indexes": []} + ) def test_unmanaged_to_managed(self): # Now, we test turning an unmanaged model into a managed model diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 3707caf106..01a9c8d886 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -51,7 +51,7 @@ class OperationTestBase(MigrationTestBase): return project_state, new_state def set_up_test_model( - self, app_label, second_model=False, third_model=False, + self, app_label, second_model=False, third_model=False, multicol_index=False, related_model=False, mti_model=False, proxy_model=False, manager_model=False, unique_together=False, options=False, db_table=None, index_together=False): """ @@ -96,6 +96,11 @@ class OperationTestBase(MigrationTestBase): ], options=model_options, )] + if multicol_index: + operations.append(migrations.AddIndex( + "Pony", + models.Index(fields=["pink", "weight"], name="pony_test_idx") + )) if second_model: operations.append(migrations.CreateModel( "Stable", @@ -1375,6 +1380,60 @@ class OperationTests(OperationTestBase): operation = migrations.AlterUniqueTogether("Pony", None) self.assertEqual(operation.describe(), "Alter unique_together for Pony (0 constraint(s))") + def test_add_index(self): + """ + Test the AddIndex operation. + """ + project_state = self.set_up_test_model("test_adin") + index = models.Index(fields=["pink"]) + operation = migrations.AddIndex("Pony", index) + self.assertEqual(operation.describe(), "Create index on field(s) pink of model Pony") + new_state = project_state.clone() + operation.state_forwards("test_adin", new_state) + # Test the database alteration + self.assertEqual(len(new_state.models["test_adin", "pony"].options['indexes']), 1) + self.assertIndexNotExists("test_adin_pony", ["pink"]) + with connection.schema_editor() as editor: + operation.database_forwards("test_adin", editor, project_state, new_state) + self.assertIndexExists("test_adin_pony", ["pink"]) + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_adin", editor, new_state, project_state) + self.assertIndexNotExists("test_adin_pony", ["pink"]) + # And deconstruction + definition = operation.deconstruct() + self.assertEqual(definition[0], "AddIndex") + self.assertEqual(definition[1], []) + self.assertEqual(definition[2], {'model_name': "Pony", 'index': index}) + + def test_remove_index(self): + """ + Test the RemoveIndex operation. + """ + project_state = self.set_up_test_model("test_rmin", multicol_index=True) + self.assertTableExists("test_rmin_pony") + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + operation = migrations.RemoveIndex("Pony", "pony_test_idx") + self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony") + new_state = project_state.clone() + operation.state_forwards("test_rmin", new_state) + # Test the state alteration + self.assertEqual(len(new_state.models["test_rmin", "pony"].options['indexes']), 0) + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + # Test the database alteration + with connection.schema_editor() as editor: + operation.database_forwards("test_rmin", editor, project_state, new_state) + self.assertIndexNotExists("test_rmin_pony", ["pink", "weight"]) + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_rmin", editor, new_state, project_state) + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + # And deconstruction + definition = operation.deconstruct() + self.assertEqual(definition[0], "RemoveIndex") + self.assertEqual(definition[1], []) + self.assertEqual(definition[2], {'model_name': "Pony", 'name': "pony_test_idx"}) + def test_alter_index_together(self): """ Tests the AlterIndexTogether operation. diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index d165221aa6..dae4fabb67 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -125,7 +125,7 @@ class StateTests(SimpleTestCase): self.assertIs(author_state.fields[3][1].null, True) self.assertEqual( author_state.options, - {"unique_together": {("name", "bio")}, "index_together": {("bio", "age")}} + {"unique_together": {("name", "bio")}, "index_together": {("bio", "age")}, "indexes": []} ) self.assertEqual(author_state.bases, (models.Model, )) @@ -135,13 +135,13 @@ class StateTests(SimpleTestCase): self.assertEqual(book_state.fields[1][1].max_length, 1000) self.assertIs(book_state.fields[2][1].null, False) self.assertEqual(book_state.fields[3][1].__class__.__name__, "ManyToManyField") - self.assertEqual(book_state.options, {"verbose_name": "tome", "db_table": "test_tome"}) + self.assertEqual(book_state.options, {"verbose_name": "tome", "db_table": "test_tome", "indexes": []}) self.assertEqual(book_state.bases, (models.Model, )) self.assertEqual(author_proxy_state.app_label, "migrations") self.assertEqual(author_proxy_state.name, "AuthorProxy") self.assertEqual(author_proxy_state.fields, []) - self.assertEqual(author_proxy_state.options, {"proxy": True, "ordering": ["name"]}) + self.assertEqual(author_proxy_state.options, {"proxy": True, "ordering": ["name"], "indexes": []}) self.assertEqual(author_proxy_state.bases, ("migrations.author", )) self.assertEqual(sub_author_state.app_label, "migrations") @@ -960,7 +960,7 @@ class ModelStateTests(SimpleTestCase): self.assertEqual(author_state.fields[1][1].max_length, 255) self.assertIs(author_state.fields[2][1].null, False) self.assertIs(author_state.fields[3][1].null, True) - self.assertEqual(author_state.options, {'swappable': 'TEST_SWAPPABLE_MODEL'}) + self.assertEqual(author_state.options, {'swappable': 'TEST_SWAPPABLE_MODEL', 'indexes': []}) self.assertEqual(author_state.bases, (models.Model, )) self.assertEqual(author_state.managers, []) diff --git a/tests/model_indexes/__init__.py b/tests/model_indexes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_indexes/models.py b/tests/model_indexes/models.py new file mode 100644 index 0000000000..598a4cb808 --- /dev/null +++ b/tests/model_indexes/models.py @@ -0,0 +1,7 @@ +from django.db import models + + +class Book(models.Model): + title = models.CharField(max_length=50) + author = models.CharField(max_length=50) + pages = models.IntegerField(db_column='page_count') diff --git a/tests/model_indexes/tests.py b/tests/model_indexes/tests.py new file mode 100644 index 0000000000..71bd8c91c0 --- /dev/null +++ b/tests/model_indexes/tests.py @@ -0,0 +1,62 @@ +from django.db import models +from django.test import TestCase + +from .models import Book + + +class IndexesTests(TestCase): + + def test_repr(self): + index = models.Index(fields=['title']) + multi_col_index = models.Index(fields=['title', 'author']) + self.assertEqual(repr(index), "") + self.assertEqual(repr(multi_col_index), "") + + def test_eq(self): + index = models.Index(fields=['title']) + same_index = models.Index(fields=['title']) + another_index = models.Index(fields=['title', 'author']) + self.assertEqual(index, same_index) + self.assertNotEqual(index, another_index) + + def test_raises_error_without_field(self): + msg = 'At least one field is required to define an index.' + with self.assertRaisesMessage(ValueError, msg): + models.Index() + + def test_max_name_length(self): + msg = 'Index names cannot be longer than 30 characters.' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='looooooooooooong_index_name_idx') + + def test_name_constraints(self): + msg = 'Index names cannot start with an underscore (_).' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='_name_starting_with_underscore') + + msg = 'Index names cannot start with a number (0-9).' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='5name_starting_with_number') + + def test_name_auto_generation(self): + index = models.Index(fields=['author']) + index.model = Book + self.assertEqual(index.name, 'model_index_author_0f5565_idx') + + # fields may be truncated in the name. db_column is used for naming. + long_field_index = models.Index(fields=['pages']) + long_field_index.model = Book + self.assertEqual(long_field_index.name, 'model_index_page_co_69235a_idx') + + # suffix can't be longer than 3 characters. + long_field_index.suffix = 'suff' + msg = 'Index too long for multiple database support. Is self.suffix longer than 3 characters?' + with self.assertRaisesMessage(AssertionError, msg): + long_field_index.get_name() + + def test_deconstruction(self): + index = models.Index(fields=['title']) + path, args, kwargs = index.deconstruct() + self.assertEqual(path, 'django.db.models.Index') + self.assertEqual(args, ()) + self.assertEqual(kwargs, {'fields': ['title']}) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 8b35b77c7b..a3e4f8227a 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -16,6 +16,7 @@ from django.db.models.fields import ( from django.db.models.fields.related import ( ForeignKey, ForeignObject, ManyToManyField, OneToOneField, ) +from django.db.models.indexes import Index from django.db.transaction import atomic from django.test import ( TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature, @@ -1443,6 +1444,26 @@ class SchemaTests(TransactionTestCase): columns = self.column_classes(Author) self.assertEqual(columns['name'][0], "CharField") + def test_add_remove_index(self): + """ + Tests index addition and removal + """ + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Author) + # Ensure the table is there and has no index + self.assertNotIn('title', self.get_indexes(Author._meta.db_table)) + # Add the index + index = Index(fields=['name'], name='author_title_idx') + index.model = Author + with connection.schema_editor() as editor: + editor.add_index(index) + self.assertIn('name', self.get_indexes(Author._meta.db_table)) + # Drop the index + with connection.schema_editor() as editor: + editor.remove_index(index) + self.assertNotIn('name', self.get_indexes(Author._meta.db_table)) + def test_indexes(self): """ Tests creation/altering of indexes