summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/contrib/postgres/indexes.py37
-rw-r--r--django/contrib/postgres/operations.py6
-rw-r--r--docs/ref/contrib/postgres/indexes.txt32
-rw-r--r--docs/ref/contrib/postgres/operations.txt9
-rw-r--r--docs/releases/2.0.txt6
-rw-r--r--tests/postgres_tests/migrations/0001_setup_extensions.py7
-rw-r--r--tests/postgres_tests/test_indexes.py70
7 files changed, 161 insertions, 6 deletions
diff --git a/django/contrib/postgres/indexes.py b/django/contrib/postgres/indexes.py
index cbfd43c1de..4156a7ded0 100644
--- a/django/contrib/postgres/indexes.py
+++ b/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
diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py
index cb9765928a..be9ed9cb23 100644
--- a/django/contrib/postgres/operations.py
+++ b/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):
diff --git a/docs/ref/contrib/postgres/indexes.txt b/docs/ref/contrib/postgres/indexes.txt
index d055a381c5..6e2a01210f 100644
--- a/docs/ref/contrib/postgres/indexes.txt
+++ b/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
diff --git a/docs/ref/contrib/postgres/operations.txt b/docs/ref/contrib/postgres/operations.txt
index d984d9a3f7..c04704eeb5 100644
--- a/docs/ref/contrib/postgres/operations.txt
+++ b/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``
===================
diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt
index 423f390433..4f92927c1e 100644
--- a/docs/releases/2.0.txt
+++ b/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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/postgres_tests/migrations/0001_setup_extensions.py b/tests/postgres_tests/migrations/0001_setup_extensions.py
index b00c6c0838..067c21849e 100644
--- a/tests/postgres_tests/migrations/0001_setup_extensions.py
+++ b/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.
diff --git a/tests/postgres_tests/test_indexes.py b/tests/postgres_tests/test_indexes.py
index ac19f53ab8..1e5dc7c380 100644
--- a/tests/postgres_tests/test_indexes.py
+++ b/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))