From f4135783add90e8e392db81b3592f4e3b9f01754 Mon Sep 17 00:00:00 2001 From: Mads Jensen Date: Sat, 1 Jul 2017 15:30:34 +0200 Subject: [PATCH] Fixed #28126 -- Added GistIndex to contrib.postgres. Thanks to Marc Tamlyn for the initial patch. --- django/contrib/postgres/indexes.py | 37 +++++++++- django/contrib/postgres/operations.py | 6 ++ docs/ref/contrib/postgres/indexes.txt | 32 +++++++++ docs/ref/contrib/postgres/operations.txt | 9 +++ docs/releases/2.0.txt | 6 ++ .../migrations/0001_setup_extensions.py | 7 +- tests/postgres_tests/test_indexes.py | 70 ++++++++++++++++++- 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 + `_. These indexes + are automatically created on spatial fields with :attr:`spatial_index=True + `. They're + also useful on other types, such as + :class:`~django.contrib.postgres.fields.HStoreField` or the :ref:`range + fields `. + + To use this index on data types not in the built-in `gist operator classes + `_, + you need to activate the `btree_gist extension + `_ 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))