mirror of https://github.com/django/django.git
Fixed #5805 -- it is now possible to specify multi-column indexes. Thanks to jgelens for the original patch.
This commit is contained in:
parent
249c3d730e
commit
4285571c5a
|
@ -1,3 +1,4 @@
|
|||
import collections
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -327,15 +328,29 @@ def get_validation_errors(outfile, app=None):
|
|||
|
||||
# Check unique_together.
|
||||
for ut in opts.unique_together:
|
||||
for field_name in ut:
|
||||
try:
|
||||
f = opts.get_field(field_name, many_to_many=True)
|
||||
except models.FieldDoesNotExist:
|
||||
e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name)
|
||||
validate_local_fields(e, opts, "unique_together", ut)
|
||||
if not isinstance(opts.index_together, collections.Sequence):
|
||||
e.add(opts, '"index_together" must a sequence')
|
||||
else:
|
||||
if isinstance(f.rel, models.ManyToManyRel):
|
||||
e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name)
|
||||
if f not in opts.local_fields:
|
||||
e.add(opts, '"unique_together" refers to %s. This is not in the same model as the unique_together statement.' % f.name)
|
||||
for it in opts.index_together:
|
||||
validate_local_fields(e, opts, "index_together", it)
|
||||
|
||||
return len(e.errors)
|
||||
|
||||
|
||||
def validate_local_fields(e, opts, field_name, fields):
|
||||
from django.db import models
|
||||
|
||||
if not isinstance(fields, collections.Sequence):
|
||||
e.add(opts, 'all %s elements must be sequences' % field_name)
|
||||
else:
|
||||
for field in fields:
|
||||
try:
|
||||
f = opts.get_field(field, many_to_many=True)
|
||||
except models.FieldDoesNotExist:
|
||||
e.add(opts, '"%s" refers to %s, a field that doesn\'t exist.' % (field_name, field))
|
||||
else:
|
||||
if isinstance(f.rel, models.ManyToManyRel):
|
||||
e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
|
||||
if f not in opts.local_fields:
|
||||
e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))
|
||||
|
|
|
@ -177,34 +177,47 @@ class BaseDatabaseCreation(object):
|
|||
output = []
|
||||
for f in model._meta.local_fields:
|
||||
output.extend(self.sql_indexes_for_field(model, f, style))
|
||||
for fs in model._meta.index_together:
|
||||
fields = [model._meta.get_field_by_name(f)[0] for f in fs]
|
||||
output.extend(self.sql_indexes_for_fields(model, fields, style))
|
||||
return output
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
"""
|
||||
Return the CREATE INDEX SQL statements for a single model field.
|
||||
"""
|
||||
if f.db_index and not f.unique:
|
||||
return self.sql_indexes_for_fields(model, [f], style)
|
||||
else:
|
||||
return []
|
||||
|
||||
def sql_indexes_for_fields(self, model, fields, style):
|
||||
from django.db.backends.util import truncate_name
|
||||
|
||||
if f.db_index and not f.unique:
|
||||
qn = self.connection.ops.quote_name
|
||||
tablespace = f.db_tablespace or model._meta.db_tablespace
|
||||
if tablespace:
|
||||
tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
|
||||
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:
|
||||
tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
|
||||
else:
|
||||
tablespace_sql = ""
|
||||
if tablespace_sql:
|
||||
tablespace_sql = ' ' + tablespace_sql
|
||||
else:
|
||||
tablespace_sql = ''
|
||||
i_name = '%s_%s' % (model._meta.db_table, self._digest(f.column))
|
||||
output = [style.SQL_KEYWORD('CREATE INDEX') + ' ' +
|
||||
style.SQL_TABLE(qn(truncate_name(
|
||||
i_name, self.connection.ops.max_name_length()))) + ' ' +
|
||||
style.SQL_KEYWORD('ON') + ' ' +
|
||||
style.SQL_TABLE(qn(model._meta.db_table)) + ' ' +
|
||||
"(%s)" % style.SQL_FIELD(qn(f.column)) +
|
||||
"%s;" % tablespace_sql]
|
||||
else:
|
||||
output = []
|
||||
return output
|
||||
tablespace_sql = " " + tablespace_sql
|
||||
|
||||
field_names = []
|
||||
qn = self.connection.ops.quote_name
|
||||
for f in fields:
|
||||
field_names.append(style.SQL_FIELD(qn(f.column)))
|
||||
|
||||
index_name = "%s_%s" % (model._meta.db_table, self._digest([f.name for f in fields]))
|
||||
|
||||
return [
|
||||
style.SQL_KEYWORD("CREATE INDEX") + " " +
|
||||
style.SQL_TABLE(qn(truncate_name(index_name, self.connection.ops.max_name_length()))) + " " +
|
||||
style.SQL_KEYWORD("ON") + " " +
|
||||
style.SQL_TABLE(qn(model._meta.db_table)) + " " +
|
||||
"(%s)" % style.SQL_FIELD(", ".join(field_names)) +
|
||||
"%s;" % tablespace_sql,
|
||||
]
|
||||
|
||||
def sql_destroy_model(self, model, references_to_delete, style):
|
||||
"""
|
||||
|
|
|
@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
|
|||
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
|
||||
'unique_together', 'permissions', 'get_latest_by',
|
||||
'order_with_respect_to', 'app_label', 'db_tablespace',
|
||||
'abstract', 'managed', 'proxy', 'swappable', 'auto_created')
|
||||
'abstract', 'managed', 'proxy', 'swappable', 'auto_created',
|
||||
'index_together')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
|
@ -34,6 +35,7 @@ class Options(object):
|
|||
self.db_table = ''
|
||||
self.ordering = []
|
||||
self.unique_together = []
|
||||
self.index_together = []
|
||||
self.permissions = []
|
||||
self.object_name, self.app_label = None, app_label
|
||||
self.get_latest_by = None
|
||||
|
|
|
@ -261,6 +261,21 @@ Django quotes column and table names behind the scenes.
|
|||
:class:`~django.db.models.ManyToManyField`, try using a signal or
|
||||
an explicit :attr:`through <ManyToManyField.through>` model.
|
||||
|
||||
``index_together``
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. attribute:: Options.index_together
|
||||
|
||||
Sets of field names that, taken together, are indexed::
|
||||
|
||||
index_together = [
|
||||
["pub_date", "deadline"],
|
||||
]
|
||||
|
||||
This list of fields will be indexed together (i.e. the appropriate
|
||||
``CREATE INDEX`` statement will be issued.)
|
||||
|
||||
``verbose_name``
|
||||
----------------
|
||||
|
||||
|
|
|
@ -356,6 +356,13 @@ class HardReferenceModel(models.Model):
|
|||
m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')
|
||||
|
||||
|
||||
class BadIndexTogether1(models.Model):
|
||||
class Meta:
|
||||
index_together = [
|
||||
["field_that_does_not_exist"],
|
||||
]
|
||||
|
||||
|
||||
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
|
||||
|
@ -470,6 +477,7 @@ invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'in
|
|||
invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
|
||||
invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'.
|
||||
invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract.
|
||||
invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist.
|
||||
"""
|
||||
|
||||
if not connection.features.interprets_empty_strings_as_nulls:
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
headline = models.CharField(max_length=100)
|
||||
pub_date = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
index_together = [
|
||||
["headline", "pub_date"],
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
from django.core.management.color import no_style
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Article
|
||||
|
||||
|
||||
class IndexesTests(TestCase):
|
||||
def test_index_together(self):
|
||||
connection = connections[DEFAULT_DB_ALIAS]
|
||||
index_sql = connection.creation.sql_indexes_for_model(Article, no_style())
|
||||
self.assertEqual(len(index_sql), 1)
|
|
@ -1,3 +1,6 @@
|
|||
from django.core.management.color import no_style
|
||||
from django.core.management.sql import custom_sql_for_model
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Simple
|
||||
|
@ -15,10 +18,6 @@ class InitialSQLTests(TestCase):
|
|||
self.assertEqual(Simple.objects.count(), 0)
|
||||
|
||||
def test_custom_sql(self):
|
||||
from django.core.management.sql import custom_sql_for_model
|
||||
from django.core.management.color import no_style
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
|
||||
# Simulate the custom SQL loading by syncdb
|
||||
connection = connections[DEFAULT_DB_ALIAS]
|
||||
custom_sql = custom_sql_for_model(Simple, no_style(), connection)
|
||||
|
|
|
@ -17,6 +17,7 @@ class Reporter(models.Model):
|
|||
def __str__(self):
|
||||
return "%s %s" % (self.first_name, self.last_name)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Article(models.Model):
|
||||
headline = models.CharField(max_length=100)
|
||||
|
@ -28,3 +29,6 @@ class Article(models.Model):
|
|||
|
||||
class Meta:
|
||||
ordering = ('headline',)
|
||||
index_together = [
|
||||
["headline", "pub_date"],
|
||||
]
|
||||
|
|
|
@ -13,7 +13,7 @@ if connection.vendor == 'oracle':
|
|||
else:
|
||||
expectedFailureOnOracle = lambda f: f
|
||||
|
||||
#
|
||||
|
||||
# The introspection module is optional, so methods tested here might raise
|
||||
# NotImplementedError. This is perfectly acceptable behavior for the backend
|
||||
# in question, but the tests need to handle this without failing. Ideally we'd
|
||||
|
@ -23,7 +23,7 @@ else:
|
|||
# wrapper that ignores the exception.
|
||||
#
|
||||
# The metaclass is just for fun.
|
||||
#
|
||||
|
||||
|
||||
def ignore_not_implemented(func):
|
||||
def _inner(*args, **kwargs):
|
||||
|
@ -34,6 +34,7 @@ def ignore_not_implemented(func):
|
|||
update_wrapper(_inner, func)
|
||||
return _inner
|
||||
|
||||
|
||||
class IgnoreNotimplementedError(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
for k, v in attrs.items():
|
||||
|
@ -41,8 +42,8 @@ class IgnoreNotimplementedError(type):
|
|||
attrs[k] = ignore_not_implemented(v)
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)):
|
||||
|
||||
class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)):
|
||||
def test_table_names(self):
|
||||
tl = connection.introspection.table_names()
|
||||
self.assertEqual(tl, sorted(tl))
|
||||
|
@ -163,6 +164,7 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)
|
|||
self.assertNotIn('first_name', indexes)
|
||||
self.assertIn('id', indexes)
|
||||
|
||||
|
||||
def datatype(dbtype, description):
|
||||
"""Helper to convert a data type into a string."""
|
||||
dt = connection.introspection.get_field_type(dbtype, description)
|
||||
|
|
Loading…
Reference in New Issue