Fixed #28126 -- Added GistIndex to contrib.postgres.
Thanks to Marc Tamlyn for the initial patch.
This commit is contained in:
parent
66657eb01f
commit
f4135783ad
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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``
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in New Issue