Fixed #25809 -- Added BrinIndex support in django.contrib.postgres.

Thanks Tim Graham and Markus Holtermann for review.
This commit is contained in:
Mads Jensen 2016-10-13 14:39:44 +02:00
parent 236ebe94bf
commit e585c43be9
8 changed files with 121 additions and 14 deletions

View File

@ -470,6 +470,7 @@ answer newbie questions, and generally made Django that much better:
Luke Plant <L.Plant.98@cantab.net>
Maciej Fijalkowski
Maciej Wiśniowski <pigletto@gmail.com>
Mads Jensen <https://github.com/atombrella>
Makoto Tsuyuki <mtsuyuki@gmail.com>
Malcolm Tredinnick
Manuel Saelices <msaelices@yaco.es>

View File

@ -1,8 +1,40 @@
from __future__ import unicode_literals
from django.db.models import Index
from django.db.models.indexes import Index
__all__ = ['GinIndex']
__all__ = ['BrinIndex', 'GinIndex']
class BrinIndex(Index):
suffix = 'brin'
def __init__(self, fields=[], name=None, pages_per_range=None):
if pages_per_range is not None and not (isinstance(pages_per_range, int) and pages_per_range > 0):
raise ValueError('pages_per_range must be None or a positive integer for BRIN indexes')
self.pages_per_range = pages_per_range
return super(BrinIndex, self).__init__(fields, name)
def __repr__(self):
if self.pages_per_range is not None:
return '<%(name)s: fields=%(fields)s, pages_per_range=%(pages_per_range)s>' % {
'name': self.__class__.__name__,
'fields': "'{}'".format(', '.join(self.fields)),
'pages_per_range': self.pages_per_range,
}
else:
return super(BrinIndex, self).__repr__()
def deconstruct(self):
path, args, kwargs = super(BrinIndex, self).deconstruct()
kwargs['pages_per_range'] = self.pages_per_range
return path, args, kwargs
def get_sql_create_template_values(self, model, schema_editor, using):
parameters = super(BrinIndex, self).get_sql_create_template_values(model, schema_editor, using=' USING brin')
if self.pages_per_range is not None:
parameters['extra'] = ' WITH (pages_per_range={})'.format(
schema_editor.quote_value(self.pages_per_range)) + parameters['extra']
return parameters
class GinIndex(Index):

View File

@ -37,6 +37,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def has_select_for_update_skip_locked(self):
return self.connection.pg_version >= 90500
@cached_property
def has_brin_index_support(self):
return self.connection.pg_version >= 90500
@cached_property
def has_jsonb_datatype(self):
return self.connection.pg_version >= 90400

View File

@ -176,13 +176,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
(SELECT fkc.relname || '.' || fka.attname
FROM pg_attribute AS fka
JOIN pg_class AS fkc ON fka.attrelid = fkc.oid
WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1])
WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1]),
cl.reloptions
FROM pg_constraint AS c
JOIN pg_class AS cl ON c.conrelid = cl.oid
JOIN pg_namespace AS ns ON cl.relnamespace = ns.oid
WHERE ns.nspname = %s AND cl.relname = %s
""", ["public", table_name])
for constraint, columns, kind, used_cols in cursor.fetchall():
for constraint, columns, kind, used_cols, options in cursor.fetchall():
constraints[constraint] = {
"columns": columns,
"primary_key": kind == "p",
@ -191,12 +192,13 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
"check": kind == "c",
"index": False,
"definition": None,
"options": options,
}
# Now get indexes
cursor.execute("""
SELECT
indexname, array_agg(attname), indisunique, indisprimary,
array_agg(ordering), amname, exprdef
array_agg(ordering), amname, exprdef, s2.attoptions
FROM (
SELECT
c2.relname as indexname, idx.*, attr.attname, am.amname,
@ -209,7 +211,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
CASE (option & 1)
WHEN 1 THEN 'DESC' ELSE 'ASC'
END
END as ordering
END as ordering,
c2.reloptions as attoptions
FROM (
SELECT
*, unnest(i.indkey) as key, unnest(i.indoption) as option
@ -221,9 +224,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
LEFT JOIN pg_attribute attr ON attr.attrelid = c.oid AND attr.attnum = idx.key
WHERE c.relname = %s
) s2
GROUP BY indexname, indisunique, indisprimary, amname, exprdef;
GROUP BY indexname, indisunique, indisprimary, amname, exprdef, attoptions;
""", [table_name])
for index, columns, unique, primary, orders, type_, definition in cursor.fetchall():
for index, columns, unique, primary, orders, type_, definition, options in cursor.fetchall():
if index not in constraints:
constraints[index] = {
"columns": columns if columns != [None] else [],
@ -235,5 +238,6 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
"index": True,
"type": type_,
"definition": definition,
"options": options,
}
return constraints

View File

@ -44,7 +44,7 @@ class Index(object):
self.name = 'D%s' % self.name[1:]
return errors
def create_sql(self, model, schema_editor, using=''):
def get_sql_create_template_values(self, model, schema_editor, using):
fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields)
quote_name = schema_editor.quote_name
@ -52,7 +52,7 @@ class Index(object):
('%s %s' % (quote_name(field.column), order)).strip()
for field, (field_name, order) in zip(fields, self.fields_orders)
]
return schema_editor.sql_create_index % {
return {
'table': quote_name(model._meta.db_table),
'name': quote_name(self.name),
'columns': ', '.join(columns),
@ -60,6 +60,11 @@ class Index(object):
'extra': tablespace_sql,
}
def create_sql(self, model, schema_editor, using='', parameters=None):
sql_create_index = schema_editor.sql_create_index
sql_parameters = parameters or self.get_sql_create_template_values(model, schema_editor, using)
return sql_create_index % sql_parameters
def remove_sql(self, model, schema_editor):
quote_name = schema_editor.quote_name
return schema_editor.sql_delete_index % {

View File

@ -9,6 +9,16 @@ PostgreSQL specific model indexes
The following are PostgreSQL specific :doc:`indexes </ref/models/indexes>`
available from the ``django.contrib.postgres.indexes`` module.
``BrinIndex``
=============
.. class:: BrinIndex(pages_per_range=None)
Creates a `BRIN index
<https://www.postgresql.org/docs/current/static/brin-intro.html>`_. For
performance considerations and use cases of the index, please consult the
documentation.
``GinIndex``
============

View File

@ -212,8 +212,9 @@ Minor features
:class:`~django.contrib.postgres.aggregates.StringAgg` determines if
concatenated values will be distinct.
* The new :class:`~django.contrib.postgres.indexes.GinIndex` class allows
creating gin indexes in the database.
* The new :class:`~django.contrib.postgres.indexes.GinIndex` and
:class:`~django.contrib.postgres.indexes.BrinIndex` classes allow
creating ``GIN`` and ``BRIN`` indexes in the database.
* :class:`~django.contrib.postgres.fields.JSONField` accepts a new ``encoder``
parameter to specify a custom class to encode data types not supported by the

View File

@ -1,8 +1,45 @@
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.indexes import BrinIndex, GinIndex
from django.db import connection
from django.test import skipUnlessDBFeature
from . import PostgreSQLTestCase
from .models import IntegerArrayModel
from .models import CharFieldModel, IntegerArrayModel
@skipUnlessDBFeature('has_brin_index_support')
class BrinIndexTests(PostgreSQLTestCase):
def test_repr(self):
index = BrinIndex(fields=['title'], pages_per_range=4)
another_index = BrinIndex(fields=['title'])
self.assertEqual(repr(index), "<BrinIndex: fields='title', pages_per_range=4>")
self.assertEqual(repr(another_index), "<BrinIndex: fields='title'>")
def test_not_eq(self):
index = BrinIndex(fields=['title'])
index_with_page_range = BrinIndex(fields=['title'], pages_per_range=16)
self.assertNotEqual(index, index_with_page_range)
def test_deconstruction(self):
index = BrinIndex(fields=['title'], name='test_title_brin')
path, args, kwargs = index.deconstruct()
self.assertEqual(path, 'django.contrib.postgres.indexes.BrinIndex')
self.assertEqual(args, ())
self.assertEqual(kwargs, {'fields': ['title'], 'name': 'test_title_brin', 'pages_per_range': None})
def test_deconstruction_with_pages_per_rank(self):
index = BrinIndex(fields=['title'], name='test_title_brin', pages_per_range=16)
path, args, kwargs = index.deconstruct()
self.assertEqual(path, 'django.contrib.postgres.indexes.BrinIndex')
self.assertEqual(args, ())
self.assertEqual(kwargs, {'fields': ['title'], 'name': 'test_title_brin', 'pages_per_range': 16})
def test_invalid_pages_per_range(self):
with self.assertRaises(ValueError):
BrinIndex(fields=['title'], name='test_title_brin', pages_per_range='Charles Babbage')
with self.assertRaises(ValueError):
BrinIndex(fields=['title'], name='test_title_brin', pages_per_range=0)
class GinIndexTests(PostgreSQLTestCase):
@ -55,3 +92,16 @@ class SchemaTests(PostgreSQLTestCase):
with connection.schema_editor() as editor:
editor.remove_index(IntegerArrayModel, index)
self.assertNotIn(index_name, self.get_constraints(IntegerArrayModel._meta.db_table))
@skipUnlessDBFeature('has_brin_index_support')
def test_brin_index(self):
index_name = 'char_field_model_field_brin'
index = BrinIndex(fields=['field'], name=index_name, pages_per_range=4)
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'], 'brin')
self.assertEqual(constraints[index_name]['options'], ['pages_per_range=4'])
with connection.schema_editor() as editor:
editor.remove_index(CharFieldModel, index)
self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))