mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #21039 -- Added AddIndexConcurrently/RemoveIndexConcurrently operations for PostgreSQL.
Thanks to Simon Charettes for review. Co-Authored-By: Daniel Tao <daniel.tao@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							9a88e43aeb
						
					
				
				
					commit
					85ac838d9e
				
			
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -214,6 +214,7 @@ answer newbie questions, and generally made Django that much better: | |||||||
|     Daniel Poelzleithner <https://poelzi.org/> |     Daniel Poelzleithner <https://poelzi.org/> | ||||||
|     Daniel Pyrathon <pirosb3@gmail.com> |     Daniel Pyrathon <pirosb3@gmail.com> | ||||||
|     Daniel Roseman <http://roseman.org.uk/> |     Daniel Roseman <http://roseman.org.uk/> | ||||||
|  |     Daniel Tao <https://philosopherdeveloper.com/> | ||||||
|     Daniel Wiesmann <daniel.wiesmann@gmail.com> |     Daniel Wiesmann <daniel.wiesmann@gmail.com> | ||||||
|     Danilo Bargen |     Danilo Bargen | ||||||
|     Dan Johnson <danj.py@gmail.com> |     Dan Johnson <danj.py@gmail.com> | ||||||
|   | |||||||
| @@ -18,9 +18,9 @@ class PostgresIndex(Index): | |||||||
|         # indexes. |         # indexes. | ||||||
|         return Index.max_name_length - len(Index.suffix) + len(self.suffix) |         return Index.max_name_length - len(Index.suffix) + len(self.suffix) | ||||||
|  |  | ||||||
|     def create_sql(self, model, schema_editor, using=''): |     def create_sql(self, model, schema_editor, using='', **kwargs): | ||||||
|         self.check_supported(schema_editor) |         self.check_supported(schema_editor) | ||||||
|         statement = super().create_sql(model, schema_editor, using=' USING %s' % self.suffix) |         statement = super().create_sql(model, schema_editor, using=' USING %s' % self.suffix, **kwargs) | ||||||
|         with_params = self.get_with_params() |         with_params = self.get_with_params() | ||||||
|         if with_params: |         if with_params: | ||||||
|             statement.parts['extra'] = 'WITH (%s) %s' % ( |             statement.parts['extra'] = 'WITH (%s) %s' % ( | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| from django.contrib.postgres.signals import ( | from django.contrib.postgres.signals import ( | ||||||
|     get_citext_oids, get_hstore_oids, register_type_handlers, |     get_citext_oids, get_hstore_oids, register_type_handlers, | ||||||
| ) | ) | ||||||
|  | from django.db.migrations import AddIndex, RemoveIndex | ||||||
| from django.db.migrations.operations.base import Operation | from django.db.migrations.operations.base import Operation | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
|  |  | ||||||
|  |  | ||||||
| class CreateExtension(Operation): | class CreateExtension(Operation): | ||||||
| @@ -75,3 +77,61 @@ class UnaccentExtension(CreateExtension): | |||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.name = 'unaccent' |         self.name = 'unaccent' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotInTransactionMixin: | ||||||
|  |     def _ensure_not_in_transaction(self, schema_editor): | ||||||
|  |         if schema_editor.connection.in_atomic_block: | ||||||
|  |             raise NotSupportedError( | ||||||
|  |                 'The %s operation cannot be executed inside a transaction ' | ||||||
|  |                 '(set atomic = False on the migration).' | ||||||
|  |                 % self.__class__.__name__ | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AddIndexConcurrently(NotInTransactionMixin, AddIndex): | ||||||
|  |     """Create an index using PostgreSQL's CREATE INDEX CONCURRENTLY syntax.""" | ||||||
|  |     atomic = False | ||||||
|  |  | ||||||
|  |     def describe(self): | ||||||
|  |         return 'Concurrently create index %s on field(s) %s of model %s' % ( | ||||||
|  |             self.index.name, | ||||||
|  |             ', '.join(self.index.fields), | ||||||
|  |             self.model_name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def database_forwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         self._ensure_not_in_transaction(schema_editor) | ||||||
|  |         model = to_state.apps.get_model(app_label, self.model_name) | ||||||
|  |         if self.allow_migrate_model(schema_editor.connection.alias, model): | ||||||
|  |             schema_editor.add_index(model, self.index, concurrently=True) | ||||||
|  |  | ||||||
|  |     def database_backwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         self._ensure_not_in_transaction(schema_editor) | ||||||
|  |         model = from_state.apps.get_model(app_label, self.model_name) | ||||||
|  |         if self.allow_migrate_model(schema_editor.connection.alias, model): | ||||||
|  |             schema_editor.remove_index(model, self.index, concurrently=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RemoveIndexConcurrently(NotInTransactionMixin, RemoveIndex): | ||||||
|  |     """Remove an index using PostgreSQL's DROP INDEX CONCURRENTLY syntax.""" | ||||||
|  |     atomic = False | ||||||
|  |  | ||||||
|  |     def describe(self): | ||||||
|  |         return 'Concurrently remove index %s from %s' % (self.name, self.model_name) | ||||||
|  |  | ||||||
|  |     def database_forwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         self._ensure_not_in_transaction(schema_editor) | ||||||
|  |         model = from_state.apps.get_model(app_label, self.model_name) | ||||||
|  |         if self.allow_migrate_model(schema_editor.connection.alias, model): | ||||||
|  |             from_model_state = from_state.models[app_label, self.model_name_lower] | ||||||
|  |             index = from_model_state.get_index_by_name(self.name) | ||||||
|  |             schema_editor.remove_index(model, index, concurrently=True) | ||||||
|  |  | ||||||
|  |     def database_backwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         self._ensure_not_in_transaction(schema_editor) | ||||||
|  |         model = to_state.apps.get_model(app_label, self.model_name) | ||||||
|  |         if self.allow_migrate_model(schema_editor.connection.alias, model): | ||||||
|  |             to_model_state = to_state.models[app_label, self.model_name_lower] | ||||||
|  |             index = to_model_state.get_index_by_name(self.name) | ||||||
|  |             schema_editor.add_index(model, index, concurrently=True) | ||||||
|   | |||||||
| @@ -959,9 +959,9 @@ class BaseDatabaseSchemaEditor: | |||||||
|             condition=(' WHERE ' + condition) if condition else '', |             condition=(' WHERE ' + condition) if condition else '', | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _delete_index_sql(self, model, name): |     def _delete_index_sql(self, model, name, sql=None): | ||||||
|         return Statement( |         return Statement( | ||||||
|             self.sql_delete_index, |             sql or self.sql_delete_index, | ||||||
|             table=Table(model._meta.db_table, self.quote_name), |             table=Table(model._meta.db_table, self.quote_name), | ||||||
|             name=self.quote_name(name), |             name=self.quote_name(name), | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -13,7 +13,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|     sql_set_sequence_owner = 'ALTER SEQUENCE %(sequence)s OWNED BY %(table)s.%(column)s' |     sql_set_sequence_owner = 'ALTER SEQUENCE %(sequence)s OWNED BY %(table)s.%(column)s' | ||||||
|  |  | ||||||
|     sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s" |     sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s" | ||||||
|  |     sql_create_index_concurrently = ( | ||||||
|  |         "CREATE INDEX CONCURRENTLY %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s" | ||||||
|  |     ) | ||||||
|     sql_delete_index = "DROP INDEX IF EXISTS %(name)s" |     sql_delete_index = "DROP INDEX IF EXISTS %(name)s" | ||||||
|  |     sql_delete_index_concurrently = "DROP INDEX CONCURRENTLY IF EXISTS %(name)s" | ||||||
|  |  | ||||||
|     sql_create_column_inline_fk = 'REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s' |     sql_create_column_inline_fk = 'REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s' | ||||||
|     # Setting the constraint to IMMEDIATE runs any deferred checks to allow |     # Setting the constraint to IMMEDIATE runs any deferred checks to allow | ||||||
| @@ -157,3 +161,24 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|         if opclasses: |         if opclasses: | ||||||
|             return IndexColumns(table, columns, self.quote_name, col_suffixes=col_suffixes, opclasses=opclasses) |             return IndexColumns(table, columns, self.quote_name, col_suffixes=col_suffixes, opclasses=opclasses) | ||||||
|         return super()._index_columns(table, columns, col_suffixes, opclasses) |         return super()._index_columns(table, columns, col_suffixes, opclasses) | ||||||
|  |  | ||||||
|  |     def add_index(self, model, index, concurrently=False): | ||||||
|  |         self.execute(index.create_sql(model, self, concurrently=concurrently), params=None) | ||||||
|  |  | ||||||
|  |     def remove_index(self, model, index, concurrently=False): | ||||||
|  |         self.execute(index.remove_sql(model, self, concurrently=concurrently)) | ||||||
|  |  | ||||||
|  |     def _delete_index_sql(self, model, name, sql=None, concurrently=False): | ||||||
|  |         sql = self.sql_delete_index_concurrently if concurrently else self.sql_delete_index | ||||||
|  |         return super()._delete_index_sql(model, name, sql) | ||||||
|  |  | ||||||
|  |     def _create_index_sql( | ||||||
|  |         self, model, fields, *, name=None, suffix='', using='', | ||||||
|  |         db_tablespace=None, col_suffixes=(), sql=None, opclasses=(), | ||||||
|  |         condition=None, concurrently=False, | ||||||
|  |     ): | ||||||
|  |         sql = self.sql_create_index if not concurrently else self.sql_create_index_concurrently | ||||||
|  |         return super()._create_index_sql( | ||||||
|  |             model, fields, name=name, suffix=suffix, using=using, db_tablespace=db_tablespace, | ||||||
|  |             col_suffixes=col_suffixes, sql=sql, opclasses=opclasses, condition=condition, | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -49,17 +49,18 @@ class Index: | |||||||
|         # it's handled outside of that class, the work is done here. |         # it's handled outside of that class, the work is done here. | ||||||
|         return sql % tuple(map(schema_editor.quote_value, params)) |         return sql % tuple(map(schema_editor.quote_value, params)) | ||||||
|  |  | ||||||
|     def create_sql(self, model, schema_editor, using=''): |     def create_sql(self, model, schema_editor, using='', **kwargs): | ||||||
|         fields = [model._meta.get_field(field_name) for field_name, _ in self.fields_orders] |         fields = [model._meta.get_field(field_name) for field_name, _ in self.fields_orders] | ||||||
|         col_suffixes = [order[1] for order in self.fields_orders] |         col_suffixes = [order[1] for order in self.fields_orders] | ||||||
|         condition = self._get_condition_sql(model, schema_editor) |         condition = self._get_condition_sql(model, schema_editor) | ||||||
|         return schema_editor._create_index_sql( |         return schema_editor._create_index_sql( | ||||||
|             model, fields, name=self.name, using=using, db_tablespace=self.db_tablespace, |             model, fields, name=self.name, using=using, db_tablespace=self.db_tablespace, | ||||||
|             col_suffixes=col_suffixes, opclasses=self.opclasses, condition=condition, |             col_suffixes=col_suffixes, opclasses=self.opclasses, condition=condition, | ||||||
|  |             **kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def remove_sql(self, model, schema_editor): |     def remove_sql(self, model, schema_editor, **kwargs): | ||||||
|         return schema_editor._delete_index_sql(model, self.name) |         return schema_editor._delete_index_sql(model, self.name, **kwargs) | ||||||
|  |  | ||||||
|     def deconstruct(self): |     def deconstruct(self): | ||||||
|         path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) |         path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) | ||||||
|   | |||||||
| @@ -98,3 +98,33 @@ run the query ``CREATE EXTENSION IF NOT EXISTS hstore;``. | |||||||
| .. class:: UnaccentExtension() | .. class:: UnaccentExtension() | ||||||
|  |  | ||||||
|     Installs the ``unaccent`` extension. |     Installs the ``unaccent`` extension. | ||||||
|  |  | ||||||
|  | Index concurrent operations | ||||||
|  | =========================== | ||||||
|  |  | ||||||
|  | .. versionadded:: 3.0 | ||||||
|  |  | ||||||
|  | PostgreSQL supports the ``CONCURRENTLY`` option to ``CREATE INDEX`` and | ||||||
|  | ``DROP INDEX`` statements to add and remove indexes without locking out writes. | ||||||
|  | This option is useful for adding or removing an index in a live production | ||||||
|  | database. | ||||||
|  |  | ||||||
|  | .. class:: AddIndexConcurrently(model_name, index) | ||||||
|  |  | ||||||
|  |     Like :class:`~django.db.migrations.operations.AddIndex`, but creates an | ||||||
|  |     index with the ``CONCURRENTLY`` option. This has a few caveats to be aware | ||||||
|  |     of when using this option, see `the PostgreSQL documentation of building | ||||||
|  |     indexes concurrently <https://www.postgresql.org/docs/current/ | ||||||
|  |     sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY>`_. | ||||||
|  |  | ||||||
|  | .. class:: RemoveIndexConcurrently(model_name, name) | ||||||
|  |  | ||||||
|  |     Like :class:`~django.db.migrations.operations.RemoveIndex`, but removes the | ||||||
|  |     index with the ``CONCURRENTLY`` option. This has a few caveats to be aware | ||||||
|  |     of when using this option, see `the PostgreSQL documentation | ||||||
|  |     <https://www.postgresql.org/docs/current/sql-dropindex.html>`_. | ||||||
|  |  | ||||||
|  | .. note:: | ||||||
|  |  | ||||||
|  |     The ``CONCURRENTLY`` option is not supported inside a transaction (see | ||||||
|  |     :ref:`non-atomic migration <non-atomic-migrations>`). | ||||||
|   | |||||||
| @@ -153,6 +153,10 @@ Minor features | |||||||
| * The new :class:`~django.contrib.postgres.fields.RangeBoundary` expression | * The new :class:`~django.contrib.postgres.fields.RangeBoundary` expression | ||||||
|   represents the range boundaries. |   represents the range boundaries. | ||||||
|  |  | ||||||
|  | * The new :class:`~django.contrib.postgres.operations.AddIndexConcurrently` | ||||||
|  |   and :class:`~django.contrib.postgres.operations.RemoveIndexConcurrently` | ||||||
|  |   classes allow creating and dropping indexes ``CONCURRENTLY`` on PostgreSQL. | ||||||
|  |  | ||||||
| :mod:`django.contrib.redirects` | :mod:`django.contrib.redirects` | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,8 +13,6 @@ from django.test import TransactionTestCase | |||||||
| from django.test.utils import extend_sys_path | from django.test.utils import extend_sys_path | ||||||
| from django.utils.module_loading import module_dir | from django.utils.module_loading import module_dir | ||||||
|  |  | ||||||
| from .models import FoodManager, FoodQuerySet |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MigrationTestBase(TransactionTestCase): | class MigrationTestBase(TransactionTestCase): | ||||||
|     """ |     """ | ||||||
| @@ -57,14 +55,14 @@ class MigrationTestBase(TransactionTestCase): | |||||||
|     def assertColumnNotNull(self, table, column, using='default'): |     def assertColumnNotNull(self, table, column, using='default'): | ||||||
|         self.assertEqual(self._get_column_allows_null(table, column, using), False) |         self.assertEqual(self._get_column_allows_null(table, column, using), False) | ||||||
|  |  | ||||||
|     def assertIndexExists(self, table, columns, value=True, using='default'): |     def assertIndexExists(self, table, columns, value=True, using='default', index_type=None): | ||||||
|         with connections[using].cursor() as cursor: |         with connections[using].cursor() as cursor: | ||||||
|             self.assertEqual( |             self.assertEqual( | ||||||
|                 value, |                 value, | ||||||
|                 any( |                 any( | ||||||
|                     c["index"] |                     c["index"] | ||||||
|                     for c in connections[using].introspection.get_constraints(cursor, table).values() |                     for c in connections[using].introspection.get_constraints(cursor, table).values() | ||||||
|                     if c['columns'] == list(columns) |                     if c['columns'] == list(columns) and (index_type is None or c['type'] == index_type) | ||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
| @@ -266,6 +264,7 @@ class OperationTestBase(MigrationTestBase): | |||||||
|                 bases=['%s.Pony' % app_label], |                 bases=['%s.Pony' % app_label], | ||||||
|             )) |             )) | ||||||
|         if manager_model: |         if manager_model: | ||||||
|  |             from .models import FoodManager, FoodQuerySet | ||||||
|             operations.append(migrations.CreateModel( |             operations.append(migrations.CreateModel( | ||||||
|                 'Food', |                 'Food', | ||||||
|                 fields=[ |                 fields=[ | ||||||
|   | |||||||
							
								
								
									
										144
									
								
								tests/postgres_tests/test_operations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								tests/postgres_tests/test_operations.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | |||||||
|  | import unittest | ||||||
|  |  | ||||||
|  | from migrations.test_base import OperationTestBase | ||||||
|  |  | ||||||
|  | from django.db import connection, models | ||||||
|  | from django.db.models import Index | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
|  | from django.test import modify_settings | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from django.contrib.postgres.operations import ( | ||||||
|  |         AddIndexConcurrently, RemoveIndexConcurrently, | ||||||
|  |     ) | ||||||
|  |     from django.contrib.postgres.indexes import BrinIndex, BTreeIndex | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific tests.') | ||||||
|  | @modify_settings(INSTALLED_APPS={'append': 'migrations'}) | ||||||
|  | class AddIndexConcurrentlyTests(OperationTestBase): | ||||||
|  |     app_label = 'test_add_concurrently' | ||||||
|  |  | ||||||
|  |     def test_requires_atomic_false(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label) | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         operation = AddIndexConcurrently( | ||||||
|  |             'Pony', | ||||||
|  |             models.Index(fields=['pink'], name='pony_pink_idx'), | ||||||
|  |         ) | ||||||
|  |         msg = ( | ||||||
|  |             'The AddIndexConcurrently operation cannot be executed inside ' | ||||||
|  |             'a transaction (set atomic = False on the migration).' | ||||||
|  |         ) | ||||||
|  |         with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |  | ||||||
|  |     def test_add(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label, index=False) | ||||||
|  |         table_name = '%s_pony' % self.app_label | ||||||
|  |         index = Index(fields=['pink'], name='pony_pink_idx') | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         operation = AddIndexConcurrently('Pony', index) | ||||||
|  |         self.assertEqual( | ||||||
|  |             operation.describe(), | ||||||
|  |             'Concurrently create index pony_pink_idx on field(s) pink of ' | ||||||
|  |             'model Pony' | ||||||
|  |         ) | ||||||
|  |         operation.state_forwards(self.app_label, new_state) | ||||||
|  |         self.assertEqual(len(new_state.models[self.app_label, 'pony'].options['indexes']), 1) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |         # Add index. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertIndexExists(table_name, ['pink']) | ||||||
|  |         # Reversal. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |         # Deconstruction. | ||||||
|  |         name, args, kwargs = operation.deconstruct() | ||||||
|  |         self.assertEqual(name, 'AddIndexConcurrently') | ||||||
|  |         self.assertEqual(args, []) | ||||||
|  |         self.assertEqual(kwargs, {'model_name': 'Pony', 'index': index}) | ||||||
|  |  | ||||||
|  |     def test_add_other_index_type(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label, index=False) | ||||||
|  |         table_name = '%s_pony' % self.app_label | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         operation = AddIndexConcurrently( | ||||||
|  |             'Pony', | ||||||
|  |             BrinIndex(fields=['pink'], name='pony_pink_brin_idx'), | ||||||
|  |         ) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |         # Add index. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertIndexExists(table_name, ['pink'], index_type='brin') | ||||||
|  |         # Reversal. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |  | ||||||
|  |     def test_add_with_options(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label, index=False) | ||||||
|  |         table_name = '%s_pony' % self.app_label | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         index = BTreeIndex(fields=['pink'], name='pony_pink_btree_idx', fillfactor=70) | ||||||
|  |         operation = AddIndexConcurrently('Pony', index) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |         # Add index. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertIndexExists(table_name, ['pink'], index_type='btree') | ||||||
|  |         # Reversal. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific tests.') | ||||||
|  | @modify_settings(INSTALLED_APPS={'append': 'migrations'}) | ||||||
|  | class RemoveIndexConcurrentlyTests(OperationTestBase): | ||||||
|  |     app_label = 'test_rm_concurrently' | ||||||
|  |  | ||||||
|  |     def test_requires_atomic_false(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label, index=True) | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         operation = RemoveIndexConcurrently('Pony', 'pony_pink_idx') | ||||||
|  |         msg = ( | ||||||
|  |             'The RemoveIndexConcurrently operation cannot be executed inside ' | ||||||
|  |             'a transaction (set atomic = False on the migration).' | ||||||
|  |         ) | ||||||
|  |         with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |  | ||||||
|  |     def test_remove(self): | ||||||
|  |         project_state = self.set_up_test_model(self.app_label, index=True) | ||||||
|  |         table_name = '%s_pony' % self.app_label | ||||||
|  |         self.assertTableExists(table_name) | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         operation = RemoveIndexConcurrently('Pony', 'pony_pink_idx') | ||||||
|  |         self.assertEqual( | ||||||
|  |             operation.describe(), | ||||||
|  |             'Concurrently remove index pony_pink_idx from Pony', | ||||||
|  |         ) | ||||||
|  |         operation.state_forwards(self.app_label, new_state) | ||||||
|  |         self.assertEqual(len(new_state.models[self.app_label, 'pony'].options['indexes']), 0) | ||||||
|  |         self.assertIndexExists(table_name, ['pink']) | ||||||
|  |         # Remove index. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertIndexNotExists(table_name, ['pink']) | ||||||
|  |         # Reversal. | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertIndexExists(table_name, ['pink']) | ||||||
|  |         # Deconstruction. | ||||||
|  |         name, args, kwargs = operation.deconstruct() | ||||||
|  |         self.assertEqual(name, 'RemoveIndexConcurrently') | ||||||
|  |         self.assertEqual(args, []) | ||||||
|  |         self.assertEqual(kwargs, {'model_name': 'Pony', 'name': 'pony_pink_idx'}) | ||||||
		Reference in New Issue
	
	Block a user