Browse Source

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

Thanks Markus Holtermann and Tim Graham for reviews.
Mariusz Felisiak 7 years ago
parent
commit
3297dede7f

+ 9 - 10
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):
         """

+ 7 - 3
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."""

+ 18 - 1
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 </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::
 
     For a list of PostgreSQL-specific indexes, see

+ 3 - 0
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
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 10 - 5
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
 ================

+ 2 - 0
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'])]

+ 44 - 4
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())