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.
This commit is contained in:
parent
c962b9104a
commit
156e2d59cf
|
@ -316,6 +316,18 @@ class BaseDatabaseSchemaEditor(object):
|
||||||
"table": self.quote_name(model._meta.db_table),
|
"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):
|
def alter_unique_together(self, model, old_unique_together, new_unique_together):
|
||||||
"""
|
"""
|
||||||
Deals with a model changing its unique_together.
|
Deals with a model changing its unique_together.
|
||||||
|
@ -836,12 +848,7 @@ class BaseDatabaseSchemaEditor(object):
|
||||||
index_name = "D%s" % index_name[:-1]
|
index_name = "D%s" % index_name[:-1]
|
||||||
return index_name
|
return index_name
|
||||||
|
|
||||||
def _create_index_sql(self, model, fields, suffix="", sql=None):
|
def _get_index_tablespace_sql(self, model, fields):
|
||||||
"""
|
|
||||||
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, ...).
|
|
||||||
"""
|
|
||||||
if len(fields) == 1 and fields[0].db_tablespace:
|
if len(fields) == 1 and fields[0].db_tablespace:
|
||||||
tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
|
tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
|
||||||
elif model._meta.db_tablespace:
|
elif model._meta.db_tablespace:
|
||||||
|
@ -850,7 +857,15 @@ class BaseDatabaseSchemaEditor(object):
|
||||||
tablespace_sql = ""
|
tablespace_sql = ""
|
||||||
if tablespace_sql:
|
if tablespace_sql:
|
||||||
tablespace_sql = " " + 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]
|
columns = [field.column for field in fields]
|
||||||
sql_create_index = sql or self.sql_create_index
|
sql_create_index = sql or self.sql_create_index
|
||||||
return sql_create_index % {
|
return sql_create_index % {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from .fields import AddField, AlterField, RemoveField, RenameField
|
from .fields import AddField, AlterField, RemoveField, RenameField
|
||||||
from .models import (
|
from .models import (
|
||||||
AlterIndexTogether, AlterModelManagers, AlterModelOptions, AlterModelTable,
|
AddIndex, AlterIndexTogether, AlterModelManagers, AlterModelOptions,
|
||||||
AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel, DeleteModel,
|
AlterModelTable, AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel,
|
||||||
RenameModel,
|
DeleteModel, RemoveIndex, RenameModel,
|
||||||
)
|
)
|
||||||
from .special import RunPython, RunSQL, SeparateDatabaseAndState
|
from .special import RunPython, RunSQL, SeparateDatabaseAndState
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
|
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
|
||||||
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
|
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions', 'AddIndex',
|
||||||
'AddField', 'RemoveField', 'AlterField', 'RenameField',
|
'RemoveIndex', 'AddField', 'RemoveField', 'AlterField', 'RenameField',
|
||||||
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
|
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
|
||||||
'AlterOrderWithRespectTo', 'AlterModelManagers',
|
'AlterOrderWithRespectTo', 'AlterModelManagers',
|
||||||
]
|
]
|
||||||
|
|
|
@ -742,3 +742,80 @@ class AlterModelManagers(ModelOptionOperation):
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
return "Change managers on %s" % (self.name, )
|
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)
|
||||||
|
|
|
@ -330,6 +330,7 @@ class ModelState(object):
|
||||||
self.name = force_text(name)
|
self.name = force_text(name)
|
||||||
self.fields = fields
|
self.fields = fields
|
||||||
self.options = options or {}
|
self.options = options or {}
|
||||||
|
self.options.setdefault('indexes', [])
|
||||||
self.bases = bases or (models.Model, )
|
self.bases = bases or (models.Model, )
|
||||||
self.managers = managers or []
|
self.managers = managers or []
|
||||||
# Sanity-check that fields is NOT a dict. It must be ordered.
|
# Sanity-check that fields is NOT a dict. It must be ordered.
|
||||||
|
@ -557,6 +558,12 @@ class ModelState(object):
|
||||||
return field
|
return field
|
||||||
raise ValueError("No field called %s on model %s" % (name, self.name))
|
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):
|
def __repr__(self):
|
||||||
return "<ModelState: '%s.%s'>" % (self.app_label, self.name)
|
return "<ModelState: '%s.%s'>" % (self.app_label, self.name)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from django.db.models.expressions import ( # NOQA
|
||||||
from django.db.models.fields import * # NOQA
|
from django.db.models.fields import * # NOQA
|
||||||
from django.db.models.fields.files import FileField, ImageField # NOQA
|
from django.db.models.fields.files import FileField, ImageField # NOQA
|
||||||
from django.db.models.fields.proxy import OrderWrt # 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.lookups import Lookup, Transform # NOQA
|
||||||
from django.db.models.manager import Manager # NOQA
|
from django.db.models.manager import Manager # NOQA
|
||||||
from django.db.models.query import ( # NOQA
|
from django.db.models.query import ( # NOQA
|
||||||
|
|
|
@ -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)
|
|
@ -43,7 +43,7 @@ DEFAULT_NAMES = (
|
||||||
'auto_created', 'index_together', 'apps', 'default_permissions',
|
'auto_created', 'index_together', 'apps', 'default_permissions',
|
||||||
'select_on_save', 'default_related_name', 'required_db_features',
|
'select_on_save', 'default_related_name', 'required_db_features',
|
||||||
'required_db_vendor', 'base_manager_name', 'default_manager_name',
|
'required_db_vendor', 'base_manager_name', 'default_manager_name',
|
||||||
'manager_inheritance_from_future',
|
'manager_inheritance_from_future', 'indexes',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ manipulating the data of your Web application. Learn more about it below:
|
||||||
* **Models:**
|
* **Models:**
|
||||||
:doc:`Introduction to models <topics/db/models>` |
|
:doc:`Introduction to models <topics/db/models>` |
|
||||||
:doc:`Field types <ref/models/fields>` |
|
:doc:`Field types <ref/models/fields>` |
|
||||||
|
:doc:`Indexes <ref/models/indexes>` |
|
||||||
:doc:`Meta options <ref/models/options>` |
|
:doc:`Meta options <ref/models/options>` |
|
||||||
:doc:`Model class <ref/models/class>`
|
:doc:`Model class <ref/models/class>`
|
||||||
|
|
||||||
|
|
|
@ -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`
|
Changes a field's name (and, unless :attr:`~django.db.models.Field.db_column`
|
||||||
is set, its column name).
|
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
|
Special Operations
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ Model API reference. For introductory material, see :doc:`/topics/db/models`.
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
fields
|
fields
|
||||||
|
indexes
|
||||||
meta
|
meta
|
||||||
relations
|
relations
|
||||||
class
|
class
|
||||||
|
|
|
@ -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.<IndexClass>``.
|
||||||
|
|
||||||
|
``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.
|
|
@ -1263,7 +1263,9 @@ class AutodetectorTests(TestCase):
|
||||||
# Right number/type of migrations?
|
# Right number/type of migrations?
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"])
|
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
|
# Now, we test turning a proxy model into a non-proxy model
|
||||||
# It should delete the proxy then make the real one
|
# It should delete the proxy then make the real one
|
||||||
changes = self.get_changes(
|
changes = self.get_changes(
|
||||||
|
@ -1273,7 +1275,7 @@ class AutodetectorTests(TestCase):
|
||||||
self.assertNumberMigrations(changes, "testapp", 1)
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
self.assertOperationTypes(changes, "testapp", 0, ["DeleteModel", "CreateModel"])
|
self.assertOperationTypes(changes, "testapp", 0, ["DeleteModel", "CreateModel"])
|
||||||
self.assertOperationAttributes(changes, "testapp", 0, 0, name="AuthorProxy")
|
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):
|
def test_proxy_custom_pk(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1296,7 +1298,9 @@ class AutodetectorTests(TestCase):
|
||||||
# Right number/type of migrations?
|
# Right number/type of migrations?
|
||||||
self.assertNumberMigrations(changes, 'testapp', 1)
|
self.assertNumberMigrations(changes, 'testapp', 1)
|
||||||
self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"])
|
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):
|
def test_unmanaged_to_managed(self):
|
||||||
# Now, we test turning an unmanaged model into a managed model
|
# Now, we test turning an unmanaged model into a managed model
|
||||||
|
|
|
@ -51,7 +51,7 @@ class OperationTestBase(MigrationTestBase):
|
||||||
return project_state, new_state
|
return project_state, new_state
|
||||||
|
|
||||||
def set_up_test_model(
|
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,
|
related_model=False, mti_model=False, proxy_model=False, manager_model=False,
|
||||||
unique_together=False, options=False, db_table=None, index_together=False):
|
unique_together=False, options=False, db_table=None, index_together=False):
|
||||||
"""
|
"""
|
||||||
|
@ -96,6 +96,11 @@ class OperationTestBase(MigrationTestBase):
|
||||||
],
|
],
|
||||||
options=model_options,
|
options=model_options,
|
||||||
)]
|
)]
|
||||||
|
if multicol_index:
|
||||||
|
operations.append(migrations.AddIndex(
|
||||||
|
"Pony",
|
||||||
|
models.Index(fields=["pink", "weight"], name="pony_test_idx")
|
||||||
|
))
|
||||||
if second_model:
|
if second_model:
|
||||||
operations.append(migrations.CreateModel(
|
operations.append(migrations.CreateModel(
|
||||||
"Stable",
|
"Stable",
|
||||||
|
@ -1375,6 +1380,60 @@ class OperationTests(OperationTestBase):
|
||||||
operation = migrations.AlterUniqueTogether("Pony", None)
|
operation = migrations.AlterUniqueTogether("Pony", None)
|
||||||
self.assertEqual(operation.describe(), "Alter unique_together for Pony (0 constraint(s))")
|
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):
|
def test_alter_index_together(self):
|
||||||
"""
|
"""
|
||||||
Tests the AlterIndexTogether operation.
|
Tests the AlterIndexTogether operation.
|
||||||
|
|
|
@ -125,7 +125,7 @@ class StateTests(SimpleTestCase):
|
||||||
self.assertIs(author_state.fields[3][1].null, True)
|
self.assertIs(author_state.fields[3][1].null, True)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
author_state.options,
|
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, ))
|
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.assertEqual(book_state.fields[1][1].max_length, 1000)
|
||||||
self.assertIs(book_state.fields[2][1].null, False)
|
self.assertIs(book_state.fields[2][1].null, False)
|
||||||
self.assertEqual(book_state.fields[3][1].__class__.__name__, "ManyToManyField")
|
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(book_state.bases, (models.Model, ))
|
||||||
|
|
||||||
self.assertEqual(author_proxy_state.app_label, "migrations")
|
self.assertEqual(author_proxy_state.app_label, "migrations")
|
||||||
self.assertEqual(author_proxy_state.name, "AuthorProxy")
|
self.assertEqual(author_proxy_state.name, "AuthorProxy")
|
||||||
self.assertEqual(author_proxy_state.fields, [])
|
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(author_proxy_state.bases, ("migrations.author", ))
|
||||||
|
|
||||||
self.assertEqual(sub_author_state.app_label, "migrations")
|
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.assertEqual(author_state.fields[1][1].max_length, 255)
|
||||||
self.assertIs(author_state.fields[2][1].null, False)
|
self.assertIs(author_state.fields[2][1].null, False)
|
||||||
self.assertIs(author_state.fields[3][1].null, True)
|
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.bases, (models.Model, ))
|
||||||
self.assertEqual(author_state.managers, [])
|
self.assertEqual(author_state.managers, [])
|
||||||
|
|
||||||
|
|
|
@ -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')
|
|
@ -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), "<Index: fields='title'>")
|
||||||
|
self.assertEqual(repr(multi_col_index), "<Index: fields='title, author'>")
|
||||||
|
|
||||||
|
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']})
|
|
@ -16,6 +16,7 @@ from django.db.models.fields import (
|
||||||
from django.db.models.fields.related import (
|
from django.db.models.fields.related import (
|
||||||
ForeignKey, ForeignObject, ManyToManyField, OneToOneField,
|
ForeignKey, ForeignObject, ManyToManyField, OneToOneField,
|
||||||
)
|
)
|
||||||
|
from django.db.models.indexes import Index
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.test import (
|
from django.test import (
|
||||||
TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature,
|
TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature,
|
||||||
|
@ -1443,6 +1444,26 @@ class SchemaTests(TransactionTestCase):
|
||||||
columns = self.column_classes(Author)
|
columns = self.column_classes(Author)
|
||||||
self.assertEqual(columns['name'][0], "CharField")
|
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):
|
def test_indexes(self):
|
||||||
"""
|
"""
|
||||||
Tests creation/altering of indexes
|
Tests creation/altering of indexes
|
||||||
|
|
Loading…
Reference in New Issue