mirror of https://github.com/django/django.git
[1.5.x] Fixed #5805 -- it is now possible to specify multi-column indexes. Thanks to jgelens for the original patch. Backport of 4285571c5a
.
This commit is contained in:
parent
26e0651c42
commit
6f716e9e5f
|
@ -1,3 +1,4 @@
|
||||||
|
import collections
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -327,15 +328,29 @@ def get_validation_errors(outfile, app=None):
|
||||||
|
|
||||||
# Check unique_together.
|
# Check unique_together.
|
||||||
for ut in opts.unique_together:
|
for ut in opts.unique_together:
|
||||||
for field_name in ut:
|
validate_local_fields(e, opts, "unique_together", ut)
|
||||||
try:
|
if not isinstance(opts.index_together, collections.Sequence):
|
||||||
f = opts.get_field(field_name, many_to_many=True)
|
e.add(opts, '"index_together" must a sequence')
|
||||||
except models.FieldDoesNotExist:
|
else:
|
||||||
e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name)
|
for it in opts.index_together:
|
||||||
else:
|
validate_local_fields(e, opts, "index_together", it)
|
||||||
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)
|
|
||||||
|
|
||||||
return len(e.errors)
|
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 = []
|
output = []
|
||||||
for f in model._meta.local_fields:
|
for f in model._meta.local_fields:
|
||||||
output.extend(self.sql_indexes_for_field(model, f, style))
|
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
|
return output
|
||||||
|
|
||||||
def sql_indexes_for_field(self, model, f, style):
|
def sql_indexes_for_field(self, model, f, style):
|
||||||
"""
|
"""
|
||||||
Return the CREATE INDEX SQL statements for a single model field.
|
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
|
from django.db.backends.util import truncate_name
|
||||||
|
|
||||||
if f.db_index and not f.unique:
|
if len(fields) == 1 and fields[0].db_tablespace:
|
||||||
qn = self.connection.ops.quote_name
|
tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
|
||||||
tablespace = f.db_tablespace or model._meta.db_tablespace
|
elif model._meta.db_tablespace:
|
||||||
if tablespace:
|
tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
|
||||||
tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
|
|
||||||
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:
|
else:
|
||||||
output = []
|
tablespace_sql = ""
|
||||||
return output
|
if tablespace_sql:
|
||||||
|
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):
|
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',
|
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
|
||||||
'unique_together', 'permissions', 'get_latest_by',
|
'unique_together', 'permissions', 'get_latest_by',
|
||||||
'order_with_respect_to', 'app_label', 'db_tablespace',
|
'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
|
@python_2_unicode_compatible
|
||||||
|
@ -34,6 +35,7 @@ class Options(object):
|
||||||
self.db_table = ''
|
self.db_table = ''
|
||||||
self.ordering = []
|
self.ordering = []
|
||||||
self.unique_together = []
|
self.unique_together = []
|
||||||
|
self.index_together = []
|
||||||
self.permissions = []
|
self.permissions = []
|
||||||
self.object_name, self.app_label = None, app_label
|
self.object_name, self.app_label = None, app_label
|
||||||
self.get_latest_by = None
|
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
|
:class:`~django.db.models.ManyToManyField`, try using a signal or
|
||||||
an explicit :attr:`through <ManyToManyField.through>` model.
|
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``
|
``verbose_name``
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -356,6 +356,13 @@ class HardReferenceModel(models.Model):
|
||||||
m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')
|
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.
|
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: "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.
|
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.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.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.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:
|
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 django.test import TestCase
|
||||||
|
|
||||||
from .models import Simple
|
from .models import Simple
|
||||||
|
@ -15,10 +18,6 @@ class InitialSQLTests(TestCase):
|
||||||
self.assertEqual(Simple.objects.count(), 0)
|
self.assertEqual(Simple.objects.count(), 0)
|
||||||
|
|
||||||
def test_custom_sql(self):
|
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
|
# Simulate the custom SQL loading by syncdb
|
||||||
connection = connections[DEFAULT_DB_ALIAS]
|
connection = connections[DEFAULT_DB_ALIAS]
|
||||||
custom_sql = custom_sql_for_model(Simple, no_style(), connection)
|
custom_sql = custom_sql_for_model(Simple, no_style(), connection)
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Reporter(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s %s" % (self.first_name, self.last_name)
|
return "%s %s" % (self.first_name, self.last_name)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
headline = models.CharField(max_length=100)
|
headline = models.CharField(max_length=100)
|
||||||
|
@ -28,3 +29,6 @@ class Article(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('headline',)
|
ordering = ('headline',)
|
||||||
|
index_together = [
|
||||||
|
["headline", "pub_date"],
|
||||||
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import,unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ if connection.vendor == 'oracle':
|
||||||
else:
|
else:
|
||||||
expectedFailureOnOracle = lambda f: f
|
expectedFailureOnOracle = lambda f: f
|
||||||
|
|
||||||
#
|
|
||||||
# The introspection module is optional, so methods tested here might raise
|
# The introspection module is optional, so methods tested here might raise
|
||||||
# NotImplementedError. This is perfectly acceptable behavior for the backend
|
# NotImplementedError. This is perfectly acceptable behavior for the backend
|
||||||
# in question, but the tests need to handle this without failing. Ideally we'd
|
# in question, but the tests need to handle this without failing. Ideally we'd
|
||||||
|
@ -23,7 +23,7 @@ else:
|
||||||
# wrapper that ignores the exception.
|
# wrapper that ignores the exception.
|
||||||
#
|
#
|
||||||
# The metaclass is just for fun.
|
# The metaclass is just for fun.
|
||||||
#
|
|
||||||
|
|
||||||
def ignore_not_implemented(func):
|
def ignore_not_implemented(func):
|
||||||
def _inner(*args, **kwargs):
|
def _inner(*args, **kwargs):
|
||||||
|
@ -34,15 +34,16 @@ def ignore_not_implemented(func):
|
||||||
update_wrapper(_inner, func)
|
update_wrapper(_inner, func)
|
||||||
return _inner
|
return _inner
|
||||||
|
|
||||||
|
|
||||||
class IgnoreNotimplementedError(type):
|
class IgnoreNotimplementedError(type):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
for k,v in attrs.items():
|
for k, v in attrs.items():
|
||||||
if k.startswith('test'):
|
if k.startswith('test'):
|
||||||
attrs[k] = ignore_not_implemented(v)
|
attrs[k] = ignore_not_implemented(v)
|
||||||
return type.__new__(cls, name, bases, attrs)
|
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):
|
def test_table_names(self):
|
||||||
tl = connection.introspection.table_names()
|
tl = connection.introspection.table_names()
|
||||||
self.assertEqual(tl, sorted(tl))
|
self.assertEqual(tl, sorted(tl))
|
||||||
|
@ -163,6 +164,7 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)
|
||||||
self.assertNotIn('first_name', indexes)
|
self.assertNotIn('first_name', indexes)
|
||||||
self.assertIn('id', indexes)
|
self.assertIn('id', indexes)
|
||||||
|
|
||||||
|
|
||||||
def datatype(dbtype, description):
|
def datatype(dbtype, description):
|
||||||
"""Helper to convert a data type into a string."""
|
"""Helper to convert a data type into a string."""
|
||||||
dt = connection.introspection.get_field_type(dbtype, description)
|
dt = connection.introspection.get_field_type(dbtype, description)
|
||||||
|
|
|
@ -277,7 +277,7 @@ if __name__ == "__main__":
|
||||||
usage = "%prog [options] [module module module ...]"
|
usage = "%prog [options] [module module module ...]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'-v','--verbosity', action='store', dest='verbosity', default='1',
|
'-v', '--verbosity', action='store', dest='verbosity', default='1',
|
||||||
type='choice', choices=['0', '1', '2', '3'],
|
type='choice', choices=['0', '1', '2', '3'],
|
||||||
help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
|
help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
|
||||||
'output')
|
'output')
|
||||||
|
|
Loading…
Reference in New Issue