From 3297dede7fce4b190f7b3bf0b0fc29a734151b61 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 27 Jun 2017 21:15:15 +0200 Subject: [PATCH] Fixed #28046 -- Added the db_tablespace parameter to class-based indexes. Thanks Markus Holtermann and Tim Graham for reviews. --- django/db/backends/base/schema.py | 19 ++++++------ django/db/models/indexes.py | 10 +++++-- docs/ref/models/indexes.txt | 19 +++++++++++- docs/releases/2.0.txt | 3 ++ docs/topics/db/tablespaces.txt | 15 ++++++---- tests/model_indexes/models.py | 2 ++ tests/model_indexes/tests.py | 48 ++++++++++++++++++++++++++++--- 7 files changed, 93 insertions(+), 23 deletions(-) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index c24e809570..ed5f2e869f 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -879,16 +879,15 @@ class BaseDatabaseSchemaEditor: index_name = "D%s" % index_name[:-1] return index_name - 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: - tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace) - else: - tablespace_sql = "" - if tablespace_sql: - tablespace_sql = " " + tablespace_sql - return tablespace_sql + def _get_index_tablespace_sql(self, model, fields, db_tablespace=None): + if db_tablespace is None: + if len(fields) == 1 and fields[0].db_tablespace: + db_tablespace = fields[0].db_tablespace + elif model._meta.db_tablespace: + db_tablespace = model._meta.db_tablespace + if db_tablespace is not None: + return ' ' + self.connection.ops.tablespace_sql(db_tablespace) + return '' def _create_index_sql(self, model, fields, suffix="", sql=None): """ diff --git a/django/db/models/indexes.py b/django/db/models/indexes.py index 4c1aeccfab..b4fc36a265 100644 --- a/django/db/models/indexes.py +++ b/django/db/models/indexes.py @@ -11,7 +11,7 @@ class Index: # cross-database compatibility with Oracle) max_name_length = 30 - def __init__(self, *, fields=[], name=None): + def __init__(self, *, fields=[], name=None, db_tablespace=None): if not isinstance(fields, list): raise ValueError('Index.fields must be a list.') if not fields: @@ -29,6 +29,7 @@ class Index: errors.append('Index names cannot be longer than %s characters.' % self.max_name_length) if errors: raise ValueError(errors) + self.db_tablespace = db_tablespace def check_name(self): errors = [] @@ -44,7 +45,7 @@ class Index: 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] - 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 columns = [ ('%s %s' % (quote_name(field.column), order)).strip() @@ -73,7 +74,10 @@ class Index: 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, '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): """Create a copy of this Index.""" diff --git a/docs/ref/models/indexes.txt b/docs/ref/models/indexes.txt index 1b2af670d7..6e8ab210ee 100644 --- a/docs/ref/models/indexes.txt +++ b/docs/ref/models/indexes.txt @@ -23,7 +23,7 @@ options`_. ``Index`` options ================= -.. class:: Index(fields=[], name=None) +.. class:: Index(fields=[], name=None, db_tablespace=None) 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 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 ` 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:: For a list of PostgreSQL-specific indexes, see diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 0b51736768..a68bd48877 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -245,6 +245,9 @@ Models function to truncate :class:`~django.db.models.DateField` and :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 ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/tablespaces.txt b/docs/topics/db/tablespaces.txt index 6cda629254..4d7151fc55 100644 --- a/docs/topics/db/tablespaces.txt +++ b/docs/topics/db/tablespaces.txt @@ -29,10 +29,12 @@ cannot control. Declaring tablespaces for indexes ================================= -You can pass the :attr:`~django.db.models.Field.db_tablespace` option to a -``Field`` constructor to specify an alternate tablespace for the ``Field``’s -column index. If no index would be created for the column, the option is -ignored. +You can pass the :attr:`~django.db.models.Index.db_tablespace` option to an +``Index`` constructor to specify the name of a tablespace to use for the index. +For single field indexes, you can pass the +: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 a default value for :attr:`~django.db.models.Field.db_tablespace`. @@ -49,17 +51,20 @@ An example class TablespaceExample(models.Model): name = models.CharField(max_length=30, db_index=True, db_tablespace="indexes") data = models.CharField(max_length=255, db_index=True) + shortcut = models.CharField(max_length=7) edges = models.ManyToManyField(to="self", db_tablespace="indexes") class Meta: db_tablespace = "tables" + indexes = [models.Index(fields=['shortcut'], db_tablespace='other_indexes')] 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`` 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 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 ================ diff --git a/tests/model_indexes/models.py b/tests/model_indexes/models.py index 6d74ad8fa6..69116b2650 100644 --- a/tests/model_indexes/models.py +++ b/tests/model_indexes/models.py @@ -5,6 +5,8 @@ class Book(models.Model): title = models.CharField(max_length=50) author = models.CharField(max_length=50) 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: indexes = [models.indexes.Index(fields=['title'])] diff --git a/tests/model_indexes/tests.py b/tests/model_indexes/tests.py index c0f5a84fdb..555b0bb0aa 100644 --- a/tests/model_indexes/tests.py +++ b/tests/model_indexes/tests.py @@ -1,5 +1,6 @@ -from django.db import models -from django.test import SimpleTestCase +from django.conf import settings +from django.db import connection, models +from django.test import SimpleTestCase, skipUnlessDBFeature from .models import Book, ChildModel1, ChildModel2 @@ -70,12 +71,15 @@ class IndexesTests(SimpleTestCase): long_field_index.set_name_with_model(Book) def test_deconstruction(self): - index = models.Index(fields=['title']) + index = models.Index(fields=['title'], db_tablespace='idx_tbls') index.set_name_with_model(Book) path, args, kwargs = index.deconstruct() self.assertEqual(path, 'django.db.models.Index') 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): index = models.Index(fields=['title']) @@ -92,3 +96,39 @@ class IndexesTests(SimpleTestCase): self.assertEqual(index_names, ['model_index_name_440998_idx']) index_names = [index.name for index in ChildModel2._meta.indexes] 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())