Fixed #25809 -- Added BrinIndex support in django.contrib.postgres.
Thanks Tim Graham and Markus Holtermann for review.
This commit is contained in:
parent
236ebe94bf
commit
e585c43be9
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 % {
|
||||
|
|
|
@ -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``
|
||||
============
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue