mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #25809 -- Added BrinIndex support in django.contrib.postgres.
Thanks Tim Graham and Markus Holtermann for review.
This commit is contained in:
		
							
								
								
									
										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)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user