소스 검색

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

Thanks Tim Graham and Markus Holtermann for review.
Mads Jensen 8 년 전
부모
커밋
e585c43be9

+ 1 - 0
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>

+ 34 - 2
django/contrib/postgres/indexes.py

@@ -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):

+ 4 - 0
django/db/backends/postgresql/features.py

@@ -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

+ 10 - 6
django/db/backends/postgresql/introspection.py

@@ -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

+ 7 - 2
django/db/models/indexes.py

@@ -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 % {

+ 10 - 0
docs/ref/contrib/postgres/indexes.txt

@@ -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``
 ============
 

+ 3 - 2
docs/releases/1.11.txt

@@ -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

+ 52 - 2
tests/postgres_tests/test_indexes.py

@@ -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))