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

Thanks to Marc Tamlyn for the initial patch.
This commit is contained in:
Mads Jensen 2017-07-01 15:30:34 +02:00 committed by Tim Graham
parent 66657eb01f
commit f4135783ad
7 changed files with 161 additions and 6 deletions

View File

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

View File

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

View File

@ -48,3 +48,35 @@ available from the ``django.contrib.postgres.indexes`` module.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
The ``fastupdate`` and ``gin_pending_list_limit`` parameters were added. 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

View File

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

View File

@ -117,6 +117,12 @@ Minor features
* :class:`django.contrib.postgres.indexes.GinIndex` now supports the * :class:`django.contrib.postgres.indexes.GinIndex` now supports the
``fastupdate`` and ``gin_pending_list_limit`` parameters. ``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` :mod:`django.contrib.redirects`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

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

View File

@ -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.db import connection
from django.test import skipUnlessDBFeature from django.test import skipUnlessDBFeature
@ -92,6 +92,46 @@ class GinIndexTests(PostgreSQLTestCase):
self.assertEqual(kwargs, {'fields': ['title'], 'name': 'test_title_gin'}) 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): class SchemaTests(PostgreSQLTestCase):
def get_constraints(self, table): def get_constraints(self, table):
@ -154,3 +194,31 @@ class SchemaTests(PostgreSQLTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.remove_index(CharFieldModel, index) editor.remove_index(CharFieldModel, index)
self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table)) 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))