Răsfoiți Sursa

Fixed #28126 -- Added GistIndex to contrib.postgres.

Thanks to Marc Tamlyn for the initial patch.
Mads Jensen 7 ani în urmă
părinte
comite
f4135783ad

+ 34 - 3
django/contrib/postgres/indexes.py

@@ -1,16 +1,19 @@
 from django.db.models import Index
 
-__all__ = ['BrinIndex', 'GinIndex']
+__all__ = ['BrinIndex', 'GinIndex', 'GistIndex']
 
 
-class BrinIndex(Index):
-    suffix = 'brin'
+class MaxLengthMixin:
     # Allow an index name longer than 30 characters since the suffix is 4
     # characters (usual limit is 3). Since this index can only be used on
     # PostgreSQL, the 30 character limit for cross-database compatibility isn't
     # applicable.
     max_name_length = 31
 
+
+class BrinIndex(MaxLengthMixin, Index):
+    suffix = 'brin'
+
     def __init__(self, *, pages_per_range=None, **kwargs):
         if pages_per_range is not None and pages_per_range <= 0:
             raise ValueError('pages_per_range must be None or a positive integer')
@@ -58,3 +61,31 @@ class GinIndex(Index):
         if with_params:
             statement.parts['extra'] = 'WITH ({}) {}'.format(', '.join(with_params), statement.parts['extra'])
         return statement
+
+
+class GistIndex(MaxLengthMixin, Index):
+    suffix = 'gist'
+
+    def __init__(self, *, buffering=None, fillfactor=None, **kwargs):
+        self.buffering = buffering
+        self.fillfactor = fillfactor
+        super().__init__(**kwargs)
+
+    def deconstruct(self):
+        path, args, kwargs = super().deconstruct()
+        if self.buffering is not None:
+            kwargs['buffering'] = self.buffering
+        if self.fillfactor is not None:
+            kwargs['fillfactor'] = self.fillfactor
+        return path, args, kwargs
+
+    def create_sql(self, model, schema_editor):
+        statement = super().create_sql(model, schema_editor, using=' USING gist')
+        with_params = []
+        if self.buffering is not None:
+            with_params.append('buffering = {}'.format('on' if self.buffering else 'off'))
+        if self.fillfactor is not None:
+            with_params.append('fillfactor = %s' % self.fillfactor)
+        if with_params:
+            statement.parts['extra'] = 'WITH ({}) {}'.format(', '.join(with_params), statement.parts['extra'])
+        return statement

+ 6 - 0
django/contrib/postgres/operations.py

@@ -33,6 +33,12 @@ class BtreeGinExtension(CreateExtension):
         self.name = 'btree_gin'
 
 
+class BtreeGistExtension(CreateExtension):
+
+    def __init__(self):
+        self.name = 'btree_gist'
+
+
 class CITextExtension(CreateExtension):
 
     def __init__(self):

+ 32 - 0
docs/ref/contrib/postgres/indexes.txt

@@ -48,3 +48,35 @@ available from the ``django.contrib.postgres.indexes`` module.
     .. versionchanged:: 2.0
 
         The ``fastupdate`` and ``gin_pending_list_limit`` parameters were added.
+
+``GistIndex``
+=============
+
+.. class:: GistIndex(buffering=None, fillfactor=None, **options)
+
+    .. versionadded:: 2.0
+
+    Creates a `GiST index
+    <https://www.postgresql.org/docs/current/static/gist.html>`_. These indexes
+    are automatically created on spatial fields with :attr:`spatial_index=True
+    <django.contrib.gis.db.models.BaseSpatialField.spatial_index>`. They're
+    also useful on other types, such as
+    :class:`~django.contrib.postgres.fields.HStoreField` or the :ref:`range
+    fields <range-fields>`.
+
+    To use this index on data types not in the built-in `gist operator classes
+    <https://www.postgresql.org/docs/current/static/gist-builtin-opclasses.html>`_,
+    you need to activate the `btree_gist extension
+    <https://www.postgresql.org/docs/current/static/btree-gist.html>`_ on
+    PostgreSQL. You can install it using the
+    :class:`~django.contrib.postgres.operations.BtreeGistExtension` migration
+    operation.
+
+    Set the ``buffering`` parameter to ``True`` or ``False`` to manually enable
+    or disable `buffering build`_ of the index.
+
+    Provide an integer value from 10 to 100 to the fillfactor_ parameter to
+    tune how packed the index pages will be. PostgreSQL's default is 90.
+
+    .. _buffering build: https://www.postgresql.org/docs/current/static/gist-implementation.html#GIST-BUFFERING-BUILD
+    .. _fillfactor: https://www.postgresql.org/docs/current/static/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS

+ 9 - 0
docs/ref/contrib/postgres/operations.txt

@@ -58,6 +58,15 @@ run the query ``CREATE EXTENSION IF NOT EXISTS hstore;``.
 
     Install the ``btree_gin`` extension.
 
+``BtreeGistExtension``
+======================
+
+.. class:: BtreeGistExtension()
+
+    .. versionadded:: 2.0
+
+    Install the ``btree_gist`` extension.
+
 ``CITextExtension``
 ===================
 

+ 6 - 0
docs/releases/2.0.txt

@@ -117,6 +117,12 @@ Minor features
 * :class:`django.contrib.postgres.indexes.GinIndex` now supports the
   ``fastupdate`` and ``gin_pending_list_limit`` parameters.
 
+* The new :class:`~django.contrib.postgres.indexes.GistIndex` class allows
+  creating ``GiST`` indexes in the database. The new
+  :class:`~django.contrib.postgres.operations.BtreeGistExtension` migration
+  operation installs the ``btree_gist`` extension to add support for operator
+  classes that aren't built-in.
+
 :mod:`django.contrib.redirects`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 5 - 2
tests/postgres_tests/migrations/0001_setup_extensions.py

@@ -4,11 +4,13 @@ from django.db import migrations
 
 try:
     from django.contrib.postgres.operations import (
-        BtreeGinExtension, CITextExtension, CreateExtension, CryptoExtension,
-        HStoreExtension, TrigramExtension, UnaccentExtension,
+        BtreeGinExtension, BtreeGistExtension, CITextExtension,
+        CreateExtension, CryptoExtension, HStoreExtension, TrigramExtension,
+        UnaccentExtension,
     )
 except ImportError:
     BtreeGinExtension = mock.Mock()
+    BtreeGistExtension = mock.Mock()
     CITextExtension = mock.Mock()
     CreateExtension = mock.Mock()
     CryptoExtension = mock.Mock()
@@ -21,6 +23,7 @@ class Migration(migrations.Migration):
 
     operations = [
         BtreeGinExtension(),
+        BtreeGistExtension(),
         CITextExtension(),
         # Ensure CreateExtension quotes extension names by creating one with a
         # dash in its name.

+ 69 - 1
tests/postgres_tests/test_indexes.py

@@ -1,4 +1,4 @@
-from django.contrib.postgres.indexes import BrinIndex, GinIndex
+from django.contrib.postgres.indexes import BrinIndex, GinIndex, GistIndex
 from django.db import connection
 from django.test import skipUnlessDBFeature
 
@@ -92,6 +92,46 @@ class GinIndexTests(PostgreSQLTestCase):
         self.assertEqual(kwargs, {'fields': ['title'], 'name': 'test_title_gin'})
 
 
+class GistIndexTests(PostgreSQLTestCase):
+
+    def test_suffix(self):
+        self.assertEqual(GistIndex.suffix, 'gist')
+
+    def test_eq(self):
+        index = GistIndex(fields=['title'], fillfactor=64)
+        same_index = GistIndex(fields=['title'], fillfactor=64)
+        another_index = GistIndex(fields=['author'], buffering=True)
+        self.assertEqual(index, same_index)
+        self.assertNotEqual(index, another_index)
+
+    def test_name_auto_generation(self):
+        index = GistIndex(fields=['field'])
+        index.set_name_with_model(CharFieldModel)
+        self.assertEqual(index.name, 'postgres_te_field_1e0206_gist')
+
+    def test_deconstruction(self):
+        index = GistIndex(fields=['title'], name='test_title_gist', buffering=False, fillfactor=80)
+        path, args, kwargs = index.deconstruct()
+        self.assertEqual(path, 'django.contrib.postgres.indexes.GistIndex')
+        self.assertEqual(args, ())
+        self.assertEqual(
+            kwargs,
+            {
+                'fields': ['title'],
+                'name': 'test_title_gist',
+                'buffering': False,
+                'fillfactor': 80,
+            }
+        )
+
+    def test_deconstruction_no_customization(self):
+        index = GistIndex(fields=['title'], name='test_title_gist')
+        path, args, kwargs = index.deconstruct()
+        self.assertEqual(path, 'django.contrib.postgres.indexes.GistIndex')
+        self.assertEqual(args, ())
+        self.assertEqual(kwargs, {'fields': ['title'], 'name': 'test_title_gist'})
+
+
 class SchemaTests(PostgreSQLTestCase):
 
     def get_constraints(self, table):
@@ -154,3 +194,31 @@ class SchemaTests(PostgreSQLTestCase):
         with connection.schema_editor() as editor:
             editor.remove_index(CharFieldModel, index)
         self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
+
+    def test_gist_index(self):
+        # Ensure the table is there and doesn't have an index.
+        self.assertNotIn('field', self.get_constraints(CharFieldModel._meta.db_table))
+        # Add the index.
+        index_name = 'char_field_model_field_gist'
+        index = GistIndex(fields=['field'], name=index_name)
+        with connection.schema_editor() as editor:
+            editor.add_index(CharFieldModel, index)
+        constraints = self.get_constraints(CharFieldModel._meta.db_table)
+        # The index was added.
+        self.assertEqual(constraints[index_name]['type'], GistIndex.suffix)
+        # Drop the index.
+        with connection.schema_editor() as editor:
+            editor.remove_index(CharFieldModel, index)
+        self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
+
+    def test_gist_parameters(self):
+        index_name = 'integer_array_gist_buffering'
+        index = GistIndex(fields=['field'], name=index_name, buffering=True, fillfactor=80)
+        with connection.schema_editor() as editor:
+            editor.add_index(CharFieldModel, index)
+        constraints = self.get_constraints(CharFieldModel._meta.db_table)
+        self.assertEqual(constraints[index_name]['type'], GistIndex.suffix)
+        self.assertEqual(constraints[index_name]['options'], ['buffering=on', 'fillfactor=80'])
+        with connection.schema_editor() as editor:
+            editor.remove_index(CharFieldModel, index)
+        self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))