mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #32046 -- Added CreateCollation/RemoveCollation operations for PostgreSQL.
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							0362b0e986
						
					
				
				
					commit
					f5e07601b2
				
			| @@ -164,3 +164,100 @@ class RemoveIndexConcurrently(NotInTransactionMixin, RemoveIndex): | |||||||
|             to_model_state = to_state.models[app_label, self.model_name_lower] |             to_model_state = to_state.models[app_label, self.model_name_lower] | ||||||
|             index = to_model_state.get_index_by_name(self.name) |             index = to_model_state.get_index_by_name(self.name) | ||||||
|             schema_editor.add_index(model, index, concurrently=True) |             schema_editor.add_index(model, index, concurrently=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CollationOperation(Operation): | ||||||
|  |     def __init__(self, name, locale, *, provider='libc', deterministic=True): | ||||||
|  |         self.name = name | ||||||
|  |         self.locale = locale | ||||||
|  |         self.provider = provider | ||||||
|  |         self.deterministic = deterministic | ||||||
|  |  | ||||||
|  |     def state_forwards(self, app_label, state): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def deconstruct(self): | ||||||
|  |         kwargs = {'name': self.name, 'locale': self.locale} | ||||||
|  |         if self.provider and self.provider != 'libc': | ||||||
|  |             kwargs['provider'] = self.provider | ||||||
|  |         if self.deterministic is False: | ||||||
|  |             kwargs['deterministic'] = self.deterministic | ||||||
|  |         return ( | ||||||
|  |             self.__class__.__qualname__, | ||||||
|  |             [], | ||||||
|  |             kwargs, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def create_collation(self, schema_editor): | ||||||
|  |         if ( | ||||||
|  |             self.deterministic is False and | ||||||
|  |             not schema_editor.connection.features.supports_non_deterministic_collations | ||||||
|  |         ): | ||||||
|  |             raise NotSupportedError( | ||||||
|  |                 'Non-deterministic collations require PostgreSQL 12+.' | ||||||
|  |             ) | ||||||
|  |         if ( | ||||||
|  |             self.provider != 'libc' and | ||||||
|  |             not schema_editor.connection.features.supports_alternate_collation_providers | ||||||
|  |         ): | ||||||
|  |             raise NotSupportedError('Non-libc providers require PostgreSQL 10+.') | ||||||
|  |         args = {'locale': schema_editor.quote_name(self.locale)} | ||||||
|  |         if self.provider != 'libc': | ||||||
|  |             args['provider'] = schema_editor.quote_name(self.provider) | ||||||
|  |         if self.deterministic is False: | ||||||
|  |             args['deterministic'] = 'false' | ||||||
|  |         schema_editor.execute('CREATE COLLATION %(name)s (%(args)s)' % { | ||||||
|  |             'name': schema_editor.quote_name(self.name), | ||||||
|  |             'args': ', '.join(f'{option}={value}' for option, value in args.items()), | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     def remove_collation(self, schema_editor): | ||||||
|  |         schema_editor.execute( | ||||||
|  |             'DROP COLLATION %s' % schema_editor.quote_name(self.name), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CreateCollation(CollationOperation): | ||||||
|  |     """Create a collation.""" | ||||||
|  |     def database_forwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         if ( | ||||||
|  |             schema_editor.connection.vendor != 'postgresql' or | ||||||
|  |             not router.allow_migrate(schema_editor.connection.alias, app_label) | ||||||
|  |         ): | ||||||
|  |             return | ||||||
|  |         self.create_collation(schema_editor) | ||||||
|  |  | ||||||
|  |     def database_backwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         if not router.allow_migrate(schema_editor.connection.alias, app_label): | ||||||
|  |             return | ||||||
|  |         self.remove_collation(schema_editor) | ||||||
|  |  | ||||||
|  |     def describe(self): | ||||||
|  |         return f'Create collation {self.name}' | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def migration_name_fragment(self): | ||||||
|  |         return 'create_collation_%s' % self.name.lower() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RemoveCollation(CollationOperation): | ||||||
|  |     """Remove a collation.""" | ||||||
|  |     def database_forwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         if ( | ||||||
|  |             schema_editor.connection.vendor != 'postgresql' or | ||||||
|  |             not router.allow_migrate(schema_editor.connection.alias, app_label) | ||||||
|  |         ): | ||||||
|  |             return | ||||||
|  |         self.remove_collation(schema_editor) | ||||||
|  |  | ||||||
|  |     def database_backwards(self, app_label, schema_editor, from_state, to_state): | ||||||
|  |         if not router.allow_migrate(schema_editor.connection.alias, app_label): | ||||||
|  |             return | ||||||
|  |         self.create_collation(schema_editor) | ||||||
|  |  | ||||||
|  |     def describe(self): | ||||||
|  |         return f'Remove collation {self.name}' | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def migration_name_fragment(self): | ||||||
|  |         return 'remove_collation_%s' % self.name.lower() | ||||||
|   | |||||||
| @@ -100,3 +100,4 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|     supports_covering_indexes = property(operator.attrgetter('is_postgresql_11')) |     supports_covering_indexes = property(operator.attrgetter('is_postgresql_11')) | ||||||
|     supports_covering_gist_indexes = property(operator.attrgetter('is_postgresql_12')) |     supports_covering_gist_indexes = property(operator.attrgetter('is_postgresql_12')) | ||||||
|     supports_non_deterministic_collations = property(operator.attrgetter('is_postgresql_12')) |     supports_non_deterministic_collations = property(operator.attrgetter('is_postgresql_12')) | ||||||
|  |     supports_alternate_collation_providers = property(operator.attrgetter('is_postgresql_10')) | ||||||
|   | |||||||
| @@ -285,6 +285,16 @@ transform do not change. For example:: | |||||||
|     .. _citext: https://www.postgresql.org/docs/current/citext.html |     .. _citext: https://www.postgresql.org/docs/current/citext.html | ||||||
|     .. _the performance considerations: https://www.postgresql.org/docs/current/citext.html#id-1.11.7.17.7 |     .. _the performance considerations: https://www.postgresql.org/docs/current/citext.html#id-1.11.7.17.7 | ||||||
|  |  | ||||||
|  | .. admonition:: Case-insensitive collations | ||||||
|  |  | ||||||
|  |     On PostgreSQL 12+, it's preferable to use non-deterministic collations | ||||||
|  |     instead of the ``citext`` extension. You can create them using the | ||||||
|  |     :class:`~django.contrib.postgres.operations.CreateCollation` migration | ||||||
|  |     operation. For more details, see :ref:`manage-postgresql-collations` and | ||||||
|  |     the PostgreSQL documentation about `non-deterministic collations`_. | ||||||
|  |  | ||||||
|  |     .. _non-deterministic collations: https://www.postgresql.org/docs/current/collation.html#COLLATION-NONDETERMINISTIC | ||||||
|  |  | ||||||
| ``HStoreField`` | ``HStoreField`` | ||||||
| =============== | =============== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -115,6 +115,56 @@ them. In that case, connect to your Django database and run the query | |||||||
|  |  | ||||||
|     Installs the ``unaccent`` extension. |     Installs the ``unaccent`` extension. | ||||||
|  |  | ||||||
|  | .. _manage-postgresql-collations: | ||||||
|  |  | ||||||
|  | Managing collations using migrations | ||||||
|  | ==================================== | ||||||
|  |  | ||||||
|  | .. versionadded:: 3.2 | ||||||
|  |  | ||||||
|  | If you need to filter or order a column using a particular collation that your | ||||||
|  | operating system provides but PostgreSQL does not, you can manage collations in | ||||||
|  | your database using a migration file. These collations can then be used with | ||||||
|  | the ``db_collation`` parameter on :class:`~django.db.models.CharField`, | ||||||
|  | :class:`~django.db.models.TextField`, and their subclasses. | ||||||
|  |  | ||||||
|  | For example, to create a collation for German phone book ordering:: | ||||||
|  |  | ||||||
|  |     from django.contrib.postgres.operations import CreateCollation | ||||||
|  |  | ||||||
|  |     class Migration(migrations.Migration): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |         operations = [ | ||||||
|  |             CreateCollation( | ||||||
|  |                 'german_phonebook', | ||||||
|  |                 provider='icu', | ||||||
|  |                 locale='und-u-ks-level2', | ||||||
|  |             ), | ||||||
|  |             ... | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  | .. class:: CreateCollation(name, locale, *, provider='libc', deterministic=True) | ||||||
|  |  | ||||||
|  |     Creates a collation with the given ``name``, ``locale`` and ``provider``. | ||||||
|  |  | ||||||
|  |     Set the ``deterministic`` parameter to ``False`` to create a | ||||||
|  |     non-deterministic collation, such as for case-insensitive filtering. | ||||||
|  |  | ||||||
|  | .. class:: RemoveCollation(name, locale, *, provider='libc', deterministic=True) | ||||||
|  |  | ||||||
|  |     Removes the collations named ``name``. | ||||||
|  |  | ||||||
|  |     When reversed this is creating a collation with the provided ``locale``, | ||||||
|  |     ``provider``, and ``deterministic`` arguments. Therefore, ``locale`` is | ||||||
|  |     required to make this operation reversible. | ||||||
|  |  | ||||||
|  | .. admonition:: Restrictions | ||||||
|  |  | ||||||
|  |     PostgreSQL 9.6 only supports the ``'libc'`` provider. | ||||||
|  |  | ||||||
|  |     Non-deterministic collations are supported only on PostgreSQL 12+. | ||||||
|  |  | ||||||
| Concurrent index operations | Concurrent index operations | ||||||
| =========================== | =========================== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -135,6 +135,11 @@ Minor features | |||||||
|   now checks that the extension already exists in the database and skips the |   now checks that the extension already exists in the database and skips the | ||||||
|   migration if so. |   migration if so. | ||||||
|  |  | ||||||
|  | * The new :class:`~django.contrib.postgres.operations.CreateCollation` and | ||||||
|  |   :class:`~django.contrib.postgres.operations.RemoveCollation` operations | ||||||
|  |   allow creating and dropping collations on PostgreSQL. See | ||||||
|  |   :ref:`manage-postgresql-collations` for more details. | ||||||
|  |  | ||||||
| :mod:`django.contrib.redirects` | :mod:`django.contrib.redirects` | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import unittest | import unittest | ||||||
|  | from unittest import mock | ||||||
|  |  | ||||||
| from migrations.test_base import OperationTestBase | from migrations.test_base import OperationTestBase | ||||||
|  |  | ||||||
| from django.db import NotSupportedError, connection | from django.db import NotSupportedError, connection | ||||||
| from django.db.migrations.state import ProjectState | from django.db.migrations.state import ProjectState | ||||||
| from django.db.models import Index | from django.db.models import Index | ||||||
| from django.test import modify_settings, override_settings | from django.db.utils import ProgrammingError | ||||||
|  | from django.test import modify_settings, override_settings, skipUnlessDBFeature | ||||||
| from django.test.utils import CaptureQueriesContext | from django.test.utils import CaptureQueriesContext | ||||||
|  |  | ||||||
| from . import PostgreSQLTestCase | from . import PostgreSQLTestCase | ||||||
| @@ -13,8 +15,8 @@ from . import PostgreSQLTestCase | |||||||
| try: | try: | ||||||
|     from django.contrib.postgres.indexes import BrinIndex, BTreeIndex |     from django.contrib.postgres.indexes import BrinIndex, BTreeIndex | ||||||
|     from django.contrib.postgres.operations import ( |     from django.contrib.postgres.operations import ( | ||||||
|         AddIndexConcurrently, BloomExtension, CreateExtension, |         AddIndexConcurrently, BloomExtension, CreateCollation, CreateExtension, | ||||||
|         RemoveIndexConcurrently, |         RemoveCollation, RemoveIndexConcurrently, | ||||||
|     ) |     ) | ||||||
| except ImportError: | except ImportError: | ||||||
|     pass |     pass | ||||||
| @@ -148,7 +150,7 @@ class RemoveIndexConcurrentlyTests(OperationTestBase): | |||||||
|         self.assertEqual(kwargs, {'model_name': 'Pony', 'name': 'pony_pink_idx'}) |         self.assertEqual(kwargs, {'model_name': 'Pony', 'name': 'pony_pink_idx'}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class NoExtensionRouter(): | class NoMigrationRouter(): | ||||||
|     def allow_migrate(self, db, app_label, **hints): |     def allow_migrate(self, db, app_label, **hints): | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
| @@ -157,7 +159,7 @@ class NoExtensionRouter(): | |||||||
| class CreateExtensionTests(PostgreSQLTestCase): | class CreateExtensionTests(PostgreSQLTestCase): | ||||||
|     app_label = 'test_allow_create_extention' |     app_label = 'test_allow_create_extention' | ||||||
|  |  | ||||||
|     @override_settings(DATABASE_ROUTERS=[NoExtensionRouter()]) |     @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) | ||||||
|     def test_no_allow_migrate(self): |     def test_no_allow_migrate(self): | ||||||
|         operation = CreateExtension('tablefunc') |         operation = CreateExtension('tablefunc') | ||||||
|         project_state = ProjectState() |         project_state = ProjectState() | ||||||
| @@ -213,3 +215,199 @@ class CreateExtensionTests(PostgreSQLTestCase): | |||||||
|                 operation.database_backwards(self.app_label, editor, project_state, new_state) |                 operation.database_backwards(self.app_label, editor, project_state, new_state) | ||||||
|         self.assertEqual(len(captured_queries), 1) |         self.assertEqual(len(captured_queries), 1) | ||||||
|         self.assertIn('SELECT', captured_queries[0]['sql']) |         self.assertIn('SELECT', captured_queries[0]['sql']) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific tests.') | ||||||
|  | class CreateCollationTests(PostgreSQLTestCase): | ||||||
|  |     app_label = 'test_allow_create_collation' | ||||||
|  |  | ||||||
|  |     @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) | ||||||
|  |     def test_no_allow_migrate(self): | ||||||
|  |         operation = CreateCollation('C_test', locale='C') | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Don't create a collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 0) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 0) | ||||||
|  |  | ||||||
|  |     def test_create(self): | ||||||
|  |         operation = CreateCollation('C_test', locale='C') | ||||||
|  |         self.assertEqual(operation.migration_name_fragment, 'create_collation_c_test') | ||||||
|  |         self.assertEqual(operation.describe(), 'Create collation C_test') | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Create a collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('CREATE COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Creating the same collation raises an exception. | ||||||
|  |         with self.assertRaisesMessage(ProgrammingError, 'already exists'): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('DROP COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Deconstruction. | ||||||
|  |         name, args, kwargs = operation.deconstruct() | ||||||
|  |         self.assertEqual(name, 'CreateCollation') | ||||||
|  |         self.assertEqual(args, []) | ||||||
|  |         self.assertEqual(kwargs, {'name': 'C_test', 'locale': 'C'}) | ||||||
|  |  | ||||||
|  |     @skipUnlessDBFeature('supports_non_deterministic_collations') | ||||||
|  |     def test_create_non_deterministic_collation(self): | ||||||
|  |         operation = CreateCollation( | ||||||
|  |             'case_insensitive_test', | ||||||
|  |             'und-u-ks-level2', | ||||||
|  |             provider='icu', | ||||||
|  |             deterministic=False, | ||||||
|  |         ) | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Create a collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('CREATE COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('DROP COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Deconstruction. | ||||||
|  |         name, args, kwargs = operation.deconstruct() | ||||||
|  |         self.assertEqual(name, 'CreateCollation') | ||||||
|  |         self.assertEqual(args, []) | ||||||
|  |         self.assertEqual(kwargs, { | ||||||
|  |             'name': 'case_insensitive_test', | ||||||
|  |             'locale': 'und-u-ks-level2', | ||||||
|  |             'provider': 'icu', | ||||||
|  |             'deterministic': False, | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     @skipUnlessDBFeature('supports_alternate_collation_providers') | ||||||
|  |     def test_create_collation_alternate_provider(self): | ||||||
|  |         operation = CreateCollation( | ||||||
|  |             'german_phonebook_test', | ||||||
|  |             provider='icu', | ||||||
|  |             locale='de-u-co-phonebk', | ||||||
|  |         ) | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Create an collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('CREATE COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('DROP COLLATION', captured_queries[0]['sql']) | ||||||
|  |  | ||||||
|  |     def test_nondeterministic_collation_not_supported(self): | ||||||
|  |         operation = CreateCollation( | ||||||
|  |             'case_insensitive_test', | ||||||
|  |             provider='icu', | ||||||
|  |             locale='und-u-ks-level2', | ||||||
|  |             deterministic=False, | ||||||
|  |         ) | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         msg = 'Non-deterministic collations require PostgreSQL 12+.' | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             with mock.patch( | ||||||
|  |                 'django.db.backends.postgresql.features.DatabaseFeatures.' | ||||||
|  |                 'supports_non_deterministic_collations', | ||||||
|  |                 False, | ||||||
|  |             ): | ||||||
|  |                 with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |                     operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |  | ||||||
|  |     def test_collation_with_icu_provider_raises_error(self): | ||||||
|  |         operation = CreateCollation( | ||||||
|  |             'german_phonebook', | ||||||
|  |             provider='icu', | ||||||
|  |             locale='de-u-co-phonebk', | ||||||
|  |         ) | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         msg = 'Non-libc providers require PostgreSQL 10+.' | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             with mock.patch( | ||||||
|  |                 'django.db.backends.postgresql.features.DatabaseFeatures.' | ||||||
|  |                 'supports_alternate_collation_providers', | ||||||
|  |                 False, | ||||||
|  |             ): | ||||||
|  |                 with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |                     operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific tests.') | ||||||
|  | class RemoveCollationTests(PostgreSQLTestCase): | ||||||
|  |     app_label = 'test_allow_remove_collation' | ||||||
|  |  | ||||||
|  |     @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) | ||||||
|  |     def test_no_allow_migrate(self): | ||||||
|  |         operation = RemoveCollation('C_test', locale='C') | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Don't create a collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 0) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 0) | ||||||
|  |  | ||||||
|  |     def test_remove(self): | ||||||
|  |         operation = CreateCollation('C_test', locale='C') | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         with connection.schema_editor(atomic=False) as editor: | ||||||
|  |             operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |  | ||||||
|  |         operation = RemoveCollation('C_test', locale='C') | ||||||
|  |         self.assertEqual(operation.migration_name_fragment, 'remove_collation_c_test') | ||||||
|  |         self.assertEqual(operation.describe(), 'Remove collation C_test') | ||||||
|  |         project_state = ProjectState() | ||||||
|  |         new_state = project_state.clone() | ||||||
|  |         # Remove a collation. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('DROP COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Removing a nonexistent collation raises an exception. | ||||||
|  |         with self.assertRaisesMessage(ProgrammingError, 'does not exist'): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 operation.database_forwards(self.app_label, editor, project_state, new_state) | ||||||
|  |         # Reversal. | ||||||
|  |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|  |             with connection.schema_editor(atomic=False) as editor: | ||||||
|  |                 operation.database_backwards(self.app_label, editor, new_state, project_state) | ||||||
|  |         self.assertEqual(len(captured_queries), 1) | ||||||
|  |         self.assertIn('CREATE COLLATION', captured_queries[0]['sql']) | ||||||
|  |         # Deconstruction. | ||||||
|  |         name, args, kwargs = operation.deconstruct() | ||||||
|  |         self.assertEqual(name, 'RemoveCollation') | ||||||
|  |         self.assertEqual(args, []) | ||||||
|  |         self.assertEqual(kwargs, {'name': 'C_test', 'locale': 'C'}) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user