Fixed #28046 -- Added the db_tablespace parameter to class-based indexes.

Thanks Markus Holtermann and Tim Graham for reviews.
This commit is contained in:
Mariusz Felisiak 2017-06-27 21:15:15 +02:00 committed by GitHub
parent 617505ca89
commit 3297dede7f
7 changed files with 93 additions and 23 deletions

View File

@ -879,16 +879,15 @@ class BaseDatabaseSchemaEditor:
index_name = "D%s" % index_name[:-1] index_name = "D%s" % index_name[:-1]
return index_name return index_name
def _get_index_tablespace_sql(self, model, fields): def _get_index_tablespace_sql(self, model, fields, db_tablespace=None):
if len(fields) == 1 and fields[0].db_tablespace: if db_tablespace is None:
tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace) if len(fields) == 1 and fields[0].db_tablespace:
elif model._meta.db_tablespace: db_tablespace = fields[0].db_tablespace
tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace) elif model._meta.db_tablespace:
else: db_tablespace = model._meta.db_tablespace
tablespace_sql = "" if db_tablespace is not None:
if tablespace_sql: return ' ' + self.connection.ops.tablespace_sql(db_tablespace)
tablespace_sql = " " + tablespace_sql return ''
return tablespace_sql
def _create_index_sql(self, model, fields, suffix="", sql=None): def _create_index_sql(self, model, fields, suffix="", sql=None):
""" """

View File

@ -11,7 +11,7 @@ class Index:
# cross-database compatibility with Oracle) # cross-database compatibility with Oracle)
max_name_length = 30 max_name_length = 30
def __init__(self, *, fields=[], name=None): def __init__(self, *, fields=[], name=None, db_tablespace=None):
if not isinstance(fields, list): if not isinstance(fields, list):
raise ValueError('Index.fields must be a list.') raise ValueError('Index.fields must be a list.')
if not fields: if not fields:
@ -29,6 +29,7 @@ class Index:
errors.append('Index names cannot be longer than %s characters.' % self.max_name_length) errors.append('Index names cannot be longer than %s characters.' % self.max_name_length)
if errors: if errors:
raise ValueError(errors) raise ValueError(errors)
self.db_tablespace = db_tablespace
def check_name(self): def check_name(self):
errors = [] errors = []
@ -44,7 +45,7 @@ class Index:
def get_sql_create_template_values(self, model, schema_editor, using): def get_sql_create_template_values(self, model, schema_editor, using):
fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders] fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields) tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields, self.db_tablespace)
quote_name = schema_editor.quote_name quote_name = schema_editor.quote_name
columns = [ columns = [
('%s %s' % (quote_name(field.column), order)).strip() ('%s %s' % (quote_name(field.column), order)).strip()
@ -73,7 +74,10 @@ class Index:
def deconstruct(self): def deconstruct(self):
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
path = path.replace('django.db.models.indexes', 'django.db.models') path = path.replace('django.db.models.indexes', 'django.db.models')
return (path, (), {'fields': self.fields, 'name': self.name}) kwargs = {'fields': self.fields, 'name': self.name}
if self.db_tablespace is not None:
kwargs['db_tablespace'] = self.db_tablespace
return (path, (), kwargs)
def clone(self): def clone(self):
"""Create a copy of this Index.""" """Create a copy of this Index."""

View File

@ -23,7 +23,7 @@ options`_.
``Index`` options ``Index`` options
================= =================
.. class:: Index(fields=[], name=None) .. class:: Index(fields=[], name=None, db_tablespace=None)
Creates an index (B-Tree) in the database. Creates an index (B-Tree) in the database.
@ -57,6 +57,23 @@ 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 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 (_). than 30 characters and shouldn't start with a number (0-9) or underscore (_).
``db_tablespace``
-----------------
.. attribute:: Index.db_tablespace
.. versionadded:: 2.0
The name of the :doc:`database tablespace </topics/db/tablespaces>` to use for
this index. For single field indexes, if ``db_tablespace`` isn't provided, the
index is created in the ``db_tablespace`` of the field.
If :attr:`.Field.db_tablespace` isn't specified (or if the index uses multiple
fields), the index is created in tablespace specified in the
:attr:`~django.db.models.Options.db_tablespace` option inside the model's
``class Meta``. If neither of those tablespaces are set, the index is created
in the same tablespace as the table.
.. seealso:: .. seealso::
For a list of PostgreSQL-specific indexes, see For a list of PostgreSQL-specific indexes, see

View File

@ -245,6 +245,9 @@ Models
function to truncate :class:`~django.db.models.DateField` and function to truncate :class:`~django.db.models.DateField` and
:class:`~django.db.models.DateTimeField` to the first day of a quarter. :class:`~django.db.models.DateTimeField` to the first day of a quarter.
* Added the :attr:`~django.db.models.Index.db_tablespace` parameter to
class-based indexes.
Requests and Responses Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

@ -29,10 +29,12 @@ cannot control.
Declaring tablespaces for indexes Declaring tablespaces for indexes
================================= =================================
You can pass the :attr:`~django.db.models.Field.db_tablespace` option to a You can pass the :attr:`~django.db.models.Index.db_tablespace` option to an
``Field`` constructor to specify an alternate tablespace for the ``Field``s ``Index`` constructor to specify the name of a tablespace to use for the index.
column index. If no index would be created for the column, the option is For single field indexes, you can pass the
ignored. :attr:`~django.db.models.Field.db_tablespace` option to a ``Field`` constructor
to specify an alternate tablespace for the field's column index. If the column
doesn't have an index, the option is ignored.
You can use the :setting:`DEFAULT_INDEX_TABLESPACE` setting to specify You can use the :setting:`DEFAULT_INDEX_TABLESPACE` setting to specify
a default value for :attr:`~django.db.models.Field.db_tablespace`. a default value for :attr:`~django.db.models.Field.db_tablespace`.
@ -49,17 +51,20 @@ An example
class TablespaceExample(models.Model): class TablespaceExample(models.Model):
name = models.CharField(max_length=30, db_index=True, db_tablespace="indexes") name = models.CharField(max_length=30, db_index=True, db_tablespace="indexes")
data = models.CharField(max_length=255, db_index=True) data = models.CharField(max_length=255, db_index=True)
shortcut = models.CharField(max_length=7)
edges = models.ManyToManyField(to="self", db_tablespace="indexes") edges = models.ManyToManyField(to="self", db_tablespace="indexes")
class Meta: class Meta:
db_tablespace = "tables" db_tablespace = "tables"
indexes = [models.Index(fields=['shortcut'], db_tablespace='other_indexes')]
In this example, the tables generated by the ``TablespaceExample`` model (i.e. In this example, the tables generated by the ``TablespaceExample`` model (i.e.
the model table and the many-to-many table) would be stored in the ``tables`` the model table and the many-to-many table) would be stored in the ``tables``
tablespace. The index for the name field and the indexes on the many-to-many tablespace. The index for the name field and the indexes on the many-to-many
table would be stored in the ``indexes`` tablespace. The ``data`` field would table would be stored in the ``indexes`` tablespace. The ``data`` field would
also generate an index, but no tablespace for it is specified, so it would be also generate an index, but no tablespace for it is specified, so it would be
stored in the model tablespace ``tables`` by default. stored in the model tablespace ``tables`` by default. The index for the
``shortcut`` field would be stored in the ``other_indexes`` tablespace.
Database support Database support
================ ================

View File

@ -5,6 +5,8 @@ class Book(models.Model):
title = models.CharField(max_length=50) title = models.CharField(max_length=50)
author = models.CharField(max_length=50) author = models.CharField(max_length=50)
pages = models.IntegerField(db_column='page_count') pages = models.IntegerField(db_column='page_count')
shortcut = models.CharField(max_length=50, db_tablespace='idx_tbls')
isbn = models.CharField(max_length=50, db_tablespace='idx_tbls')
class Meta: class Meta:
indexes = [models.indexes.Index(fields=['title'])] indexes = [models.indexes.Index(fields=['title'])]

View File

@ -1,5 +1,6 @@
from django.db import models from django.conf import settings
from django.test import SimpleTestCase from django.db import connection, models
from django.test import SimpleTestCase, skipUnlessDBFeature
from .models import Book, ChildModel1, ChildModel2 from .models import Book, ChildModel1, ChildModel2
@ -70,12 +71,15 @@ class IndexesTests(SimpleTestCase):
long_field_index.set_name_with_model(Book) long_field_index.set_name_with_model(Book)
def test_deconstruction(self): def test_deconstruction(self):
index = models.Index(fields=['title']) index = models.Index(fields=['title'], db_tablespace='idx_tbls')
index.set_name_with_model(Book) index.set_name_with_model(Book)
path, args, kwargs = index.deconstruct() path, args, kwargs = index.deconstruct()
self.assertEqual(path, 'django.db.models.Index') self.assertEqual(path, 'django.db.models.Index')
self.assertEqual(args, ()) self.assertEqual(args, ())
self.assertEqual(kwargs, {'fields': ['title'], 'name': 'model_index_title_196f42_idx'}) self.assertEqual(
kwargs,
{'fields': ['title'], 'name': 'model_index_title_196f42_idx', 'db_tablespace': 'idx_tbls'}
)
def test_clone(self): def test_clone(self):
index = models.Index(fields=['title']) index = models.Index(fields=['title'])
@ -92,3 +96,39 @@ class IndexesTests(SimpleTestCase):
self.assertEqual(index_names, ['model_index_name_440998_idx']) self.assertEqual(index_names, ['model_index_name_440998_idx'])
index_names = [index.name for index in ChildModel2._meta.indexes] index_names = [index.name for index in ChildModel2._meta.indexes]
self.assertEqual(index_names, ['model_index_name_b6c374_idx']) self.assertEqual(index_names, ['model_index_name_b6c374_idx'])
@skipUnlessDBFeature('supports_tablespaces')
def test_db_tablespace(self):
with connection.schema_editor() as editor:
# Index with db_tablespace attribute.
for fields in [
# Field with db_tablespace specified on model.
['shortcut'],
# Field without db_tablespace specified on model.
['author'],
# Multi-column with db_tablespaces specified on model.
['shortcut', 'isbn'],
# Multi-column without db_tablespace specified on model.
['title', 'author'],
]:
with self.subTest(fields=fields):
index = models.Index(fields=fields, db_tablespace='idx_tbls2')
self.assertIn('"idx_tbls2"', index.create_sql(Book, editor).lower())
# Indexes without db_tablespace attribute.
for fields in [['author'], ['shortcut', 'isbn'], ['title', 'author']]:
with self.subTest(fields=fields):
index = models.Index(fields=fields)
# The DEFAULT_INDEX_TABLESPACE setting can't be tested
# because it's evaluated when the model class is defined.
# As a consequence, @override_settings doesn't work.
if settings.DEFAULT_INDEX_TABLESPACE:
self.assertIn(
'"%s"' % settings.DEFAULT_INDEX_TABLESPACE,
index.create_sql(Book, editor).lower()
)
else:
self.assertNotIn('TABLESPACE', index.create_sql(Book, editor))
# Field with db_tablespace specified on the model and an index
# without db_tablespace.
index = models.Index(fields=['shortcut'])
self.assertIn('"idx_tbls"', index.create_sql(Book, editor).lower())