mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[2.0.x] Fixed #28849 -- Fixed referenced table and column rename on SQLite.
Thanks Ramiro for the input and Tim for the review.
Backport of 095c1aaa89 from master
			
			
This commit is contained in:
		| @@ -123,7 +123,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor): | |||||||
|         else: |         else: | ||||||
|             super().remove_field(model, field) |             super().remove_field(model, field) | ||||||
|  |  | ||||||
|     def alter_db_table(self, model, old_db_table, new_db_table): |     def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True): | ||||||
|         from django.contrib.gis.db.models.fields import GeometryField |         from django.contrib.gis.db.models.fields import GeometryField | ||||||
|         # Remove geometry-ness from temp table |         # Remove geometry-ness from temp table | ||||||
|         for field in model._meta.local_fields: |         for field in model._meta.local_fields: | ||||||
| @@ -135,7 +135,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor): | |||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|         # Alter table |         # Alter table | ||||||
|         super().alter_db_table(model, old_db_table, new_db_table) |         super().alter_db_table(model, old_db_table, new_db_table, disable_constraints) | ||||||
|         # Repoint any straggler names |         # Repoint any straggler names | ||||||
|         for geom_table in self.geometry_tables: |         for geom_table in self.geometry_tables: | ||||||
|             try: |             try: | ||||||
|   | |||||||
| @@ -167,6 +167,9 @@ class BaseDatabaseFeatures: | |||||||
|     # Can we roll back DDL in a transaction? |     # Can we roll back DDL in a transaction? | ||||||
|     can_rollback_ddl = False |     can_rollback_ddl = False | ||||||
|  |  | ||||||
|  |     # Does it support operations requiring references rename in a transaction? | ||||||
|  |     supports_atomic_references_rename = True | ||||||
|  |  | ||||||
|     # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? |     # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? | ||||||
|     supports_combined_alters = False |     supports_combined_alters = False | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|     supports_transactions = True |     supports_transactions = True | ||||||
|     atomic_transactions = False |     atomic_transactions = False | ||||||
|     can_rollback_ddl = True |     can_rollback_ddl = True | ||||||
|  |     supports_atomic_references_rename = False | ||||||
|     supports_paramstyle_pyformat = False |     supports_paramstyle_pyformat = False | ||||||
|     supports_sequence_reset = False |     supports_sequence_reset = False | ||||||
|     can_clone_databases = True |     can_clone_databases = True | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ from decimal import Decimal | |||||||
| from django.apps.registry import Apps | from django.apps.registry import Apps | ||||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||||
| from django.db.backends.ddl_references import Statement | from django.db.backends.ddl_references import Statement | ||||||
|  | from django.db.transaction import atomic | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
|  |  | ||||||
|  |  | ||||||
| class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | ||||||
| @@ -59,6 +61,58 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|         else: |         else: | ||||||
|             raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value))) |             raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value))) | ||||||
|  |  | ||||||
|  |     def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True): | ||||||
|  |         if model._meta.related_objects and disable_constraints: | ||||||
|  |             if self.connection.in_atomic_block: | ||||||
|  |                 raise NotSupportedError(( | ||||||
|  |                     'Renaming the %r table while in a transaction is not ' | ||||||
|  |                     'supported on SQLite because it would break referential ' | ||||||
|  |                     'integrity. Try adding `atomic = False` to the Migration class.' | ||||||
|  |                 ) % old_db_table) | ||||||
|  |             self.connection.enable_constraint_checking() | ||||||
|  |             super().alter_db_table(model, old_db_table, new_db_table) | ||||||
|  |             self.connection.disable_constraint_checking() | ||||||
|  |         else: | ||||||
|  |             super().alter_db_table(model, old_db_table, new_db_table) | ||||||
|  |  | ||||||
|  |     def alter_field(self, model, old_field, new_field, strict=False): | ||||||
|  |         old_field_name = old_field.name | ||||||
|  |         if (new_field.name != old_field_name and | ||||||
|  |                 any(r.field_name == old_field.name for r in model._meta.related_objects)): | ||||||
|  |             if self.connection.in_atomic_block: | ||||||
|  |                 raise NotSupportedError(( | ||||||
|  |                     'Renaming the %r.%r column while in a transaction is not ' | ||||||
|  |                     'supported on SQLite because it would break referential ' | ||||||
|  |                     'integrity. Try adding `atomic = False` to the Migration class.' | ||||||
|  |                 ) % (model._meta.db_table, old_field_name)) | ||||||
|  |             with atomic(self.connection.alias): | ||||||
|  |                 super().alter_field(model, old_field, new_field, strict=strict) | ||||||
|  |                 # Follow SQLite's documented procedure for performing changes | ||||||
|  |                 # that don't affect the on-disk content. | ||||||
|  |                 # https://sqlite.org/lang_altertable.html#otheralter | ||||||
|  |                 with self.connection.cursor() as cursor: | ||||||
|  |                     schema_version = cursor.execute('PRAGMA schema_version').fetchone()[0] | ||||||
|  |                     cursor.execute('PRAGMA writable_schema = 1') | ||||||
|  |                     table_name = model._meta.db_table | ||||||
|  |                     references_template = ' REFERENCES "%s" ("%%s") ' % table_name | ||||||
|  |                     old_column_name = old_field.get_attname_column()[1] | ||||||
|  |                     new_column_name = new_field.get_attname_column()[1] | ||||||
|  |                     search = references_template % old_column_name | ||||||
|  |                     replacement = references_template % new_column_name | ||||||
|  |                     cursor.execute('UPDATE sqlite_master SET sql = replace(sql, %s, %s)', (search, replacement)) | ||||||
|  |                     cursor.execute('PRAGMA schema_version = %d' % (schema_version + 1)) | ||||||
|  |                     cursor.execute('PRAGMA writable_schema = 0') | ||||||
|  |                     # The integrity check will raise an exception and rollback | ||||||
|  |                     # the transaction if the sqlite_master updates corrupt the | ||||||
|  |                     # database. | ||||||
|  |                     cursor.execute('PRAGMA integrity_check') | ||||||
|  |             # Perform a VACUUM to refresh the database representation from | ||||||
|  |             # the sqlite_master table. | ||||||
|  |             with self.connection.cursor() as cursor: | ||||||
|  |                 cursor.execute('VACUUM') | ||||||
|  |         else: | ||||||
|  |             super().alter_field(model, old_field, new_field, strict=strict) | ||||||
|  |  | ||||||
|     def _remake_table(self, model, create_field=None, delete_field=None, alter_field=None): |     def _remake_table(self, model, create_field=None, delete_field=None, alter_field=None): | ||||||
|         """ |         """ | ||||||
|         Shortcut to transform a model from old_model into new_model |         Shortcut to transform a model from old_model into new_model | ||||||
| @@ -182,8 +236,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|  |  | ||||||
|         with altered_table_name(model, model._meta.db_table + "__old"): |         with altered_table_name(model, model._meta.db_table + "__old"): | ||||||
|             # Rename the old table to make way for the new |             # Rename the old table to make way for the new | ||||||
|             self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table) |             self.alter_db_table( | ||||||
|  |                 model, temp_model._meta.db_table, model._meta.db_table, | ||||||
|  |                 disable_constraints=False, | ||||||
|  |             ) | ||||||
|             # Create a new table with the updated schema. |             # Create a new table with the updated schema. | ||||||
|             self.create_model(temp_model) |             self.create_model(temp_model) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -534,6 +534,13 @@ rebuild tables using a script similar to this:: | |||||||
| This script hasn't received extensive testing and needs adaption for various | This script hasn't received extensive testing and needs adaption for various | ||||||
| cases such as multiple databases. Feel free to contribute improvements. | cases such as multiple databases. Feel free to contribute improvements. | ||||||
|  |  | ||||||
|  | In addition, because of a table alteration limitation of SQLite, it's prohibited | ||||||
|  | to perform :class:`~django.db.migrations.operations.RenameModel` and | ||||||
|  | :class:`~django.db.migrations.operations.RenameField` operations on models or | ||||||
|  | fields referenced by other models in a transaction. In order to allow migrations | ||||||
|  | containing these operations to be applied, you must set the | ||||||
|  | ``Migration.atomic`` attribute to ``False``. | ||||||
|  |  | ||||||
| Miscellaneous | Miscellaneous | ||||||
| ------------- | ------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -103,3 +103,11 @@ class ObjectReference(models.Model): | |||||||
|  |  | ||||||
| class RawData(models.Model): | class RawData(models.Model): | ||||||
|     raw_data = models.BinaryField() |     raw_data = models.BinaryField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Author(models.Model): | ||||||
|  |     name = models.CharField(max_length=255, unique=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Book(models.Model): | ||||||
|  |     author = models.ForeignKey(Author, models.CASCADE, to_field='name') | ||||||
|   | |||||||
| @@ -5,11 +5,14 @@ import unittest | |||||||
| from django.core.exceptions import ImproperlyConfigured | from django.core.exceptions import ImproperlyConfigured | ||||||
| from django.db import connection | from django.db import connection | ||||||
| from django.db.models import Avg, StdDev, Sum, Variance | from django.db.models import Avg, StdDev, Sum, Variance | ||||||
|  | from django.db.models.fields import CharField | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
| from django.test import ( | from django.test import ( | ||||||
|     TestCase, TransactionTestCase, override_settings, skipUnlessDBFeature, |     TestCase, TransactionTestCase, override_settings, skipUnlessDBFeature, | ||||||
| ) | ) | ||||||
|  | from django.test.utils import isolate_apps | ||||||
|  |  | ||||||
| from ..models import Item, Object, Square | from ..models import Author, Item, Object, Square | ||||||
|  |  | ||||||
|  |  | ||||||
| @unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite tests') | @unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite tests') | ||||||
| @@ -71,6 +74,44 @@ class Tests(TestCase): | |||||||
|                 creation._get_test_db_name() |                 creation._get_test_db_name() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite tests') | ||||||
|  | @isolate_apps('backends') | ||||||
|  | class SchemaTests(TransactionTestCase): | ||||||
|  |  | ||||||
|  |     available_apps = ['backends'] | ||||||
|  |  | ||||||
|  |     def test_field_rename_inside_atomic_block(self): | ||||||
|  |         """ | ||||||
|  |         NotImplementedError is raised when a model field rename is attempted | ||||||
|  |         inside an atomic block. | ||||||
|  |         """ | ||||||
|  |         new_field = CharField(max_length=255, unique=True) | ||||||
|  |         new_field.set_attributes_from_name('renamed') | ||||||
|  |         msg = ( | ||||||
|  |             "Renaming the 'backends_author'.'name' column while in a " | ||||||
|  |             "transaction is not supported on SQLite because it would break " | ||||||
|  |             "referential integrity. Try adding `atomic = False` to the " | ||||||
|  |             "Migration class." | ||||||
|  |         ) | ||||||
|  |         with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 editor.alter_field(Author, Author._meta.get_field('name'), new_field) | ||||||
|  |  | ||||||
|  |     def test_table_rename_inside_atomic_block(self): | ||||||
|  |         """ | ||||||
|  |         NotImplementedError is raised when a table rename is attempted inside | ||||||
|  |         an atomic block. | ||||||
|  |         """ | ||||||
|  |         msg = ( | ||||||
|  |             "Renaming the 'backends_author' table while in a transaction is " | ||||||
|  |             "not supported on SQLite because it would break referential " | ||||||
|  |             "integrity. Try adding `atomic = False` to the Migration class." | ||||||
|  |         ) | ||||||
|  |         with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|  |             with connection.schema_editor(atomic=True) as editor: | ||||||
|  |                 editor.alter_db_table(Author, "backends_author", "renamed_table") | ||||||
|  |  | ||||||
|  |  | ||||||
| @unittest.skipUnless(connection.vendor == 'sqlite', 'Test only for SQLite') | @unittest.skipUnless(connection.vendor == 'sqlite', 'Test only for SQLite') | ||||||
| @override_settings(DEBUG=True) | @override_settings(DEBUG=True) | ||||||
| class LastExecutedQueryTest(TestCase): | class LastExecutedQueryTest(TestCase): | ||||||
|   | |||||||
| @@ -28,16 +28,16 @@ class OperationTestBase(MigrationTestBase): | |||||||
|     Common functions to help test operations. |     Common functions to help test operations. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def apply_operations(self, app_label, project_state, operations): |     def apply_operations(self, app_label, project_state, operations, atomic=True): | ||||||
|         migration = Migration('name', app_label) |         migration = Migration('name', app_label) | ||||||
|         migration.operations = operations |         migration.operations = operations | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=atomic) as editor: | ||||||
|             return migration.apply(project_state, editor) |             return migration.apply(project_state, editor) | ||||||
|  |  | ||||||
|     def unapply_operations(self, app_label, project_state, operations): |     def unapply_operations(self, app_label, project_state, operations, atomic=True): | ||||||
|         migration = Migration('name', app_label) |         migration = Migration('name', app_label) | ||||||
|         migration.operations = operations |         migration.operations = operations | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=atomic) as editor: | ||||||
|             return migration.unapply(project_state, editor) |             return migration.unapply(project_state, editor) | ||||||
|  |  | ||||||
|     def make_test_state(self, app_label, operation, **kwargs): |     def make_test_state(self, app_label, operation, **kwargs): | ||||||
| @@ -561,7 +561,8 @@ class OperationTests(OperationTestBase): | |||||||
|             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) |             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) | ||||||
|         # Migrate forwards |         # Migrate forwards | ||||||
|         new_state = project_state.clone() |         new_state = project_state.clone() | ||||||
|         new_state = self.apply_operations("test_rnmo", new_state, [operation]) |         atomic_rename = connection.features.supports_atomic_references_rename | ||||||
|  |         new_state = self.apply_operations("test_rnmo", new_state, [operation], atomic=atomic_rename) | ||||||
|         # Test new state and database |         # Test new state and database | ||||||
|         self.assertNotIn(("test_rnmo", "pony"), new_state.models) |         self.assertNotIn(("test_rnmo", "pony"), new_state.models) | ||||||
|         self.assertIn(("test_rnmo", "horse"), new_state.models) |         self.assertIn(("test_rnmo", "horse"), new_state.models) | ||||||
| @@ -573,7 +574,7 @@ class OperationTests(OperationTestBase): | |||||||
|             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_pony", "id")) |             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_pony", "id")) | ||||||
|             self.assertFKExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) |             self.assertFKExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) | ||||||
|         # Migrate backwards |         # Migrate backwards | ||||||
|         original_state = self.unapply_operations("test_rnmo", project_state, [operation]) |         original_state = self.unapply_operations("test_rnmo", project_state, [operation], atomic=atomic_rename) | ||||||
|         # Test original state and database |         # Test original state and database | ||||||
|         self.assertIn(("test_rnmo", "pony"), original_state.models) |         self.assertIn(("test_rnmo", "pony"), original_state.models) | ||||||
|         self.assertNotIn(("test_rnmo", "horse"), original_state.models) |         self.assertNotIn(("test_rnmo", "horse"), original_state.models) | ||||||
| @@ -634,7 +635,8 @@ class OperationTests(OperationTestBase): | |||||||
|         if connection.features.supports_foreign_keys: |         if connection.features.supports_foreign_keys: | ||||||
|             self.assertFKExists("test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_rider", "id")) |             self.assertFKExists("test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_rider", "id")) | ||||||
|             self.assertFKNotExists("test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_horserider", "id")) |             self.assertFKNotExists("test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_horserider", "id")) | ||||||
|         with connection.schema_editor() as editor: |         atomic_rename = connection.features.supports_atomic_references_rename | ||||||
|  |         with connection.schema_editor(atomic=atomic_rename) as editor: | ||||||
|             operation.database_forwards("test_rmwsrf", editor, project_state, new_state) |             operation.database_forwards("test_rmwsrf", editor, project_state, new_state) | ||||||
|         self.assertTableNotExists("test_rmwsrf_rider") |         self.assertTableNotExists("test_rmwsrf_rider") | ||||||
|         self.assertTableExists("test_rmwsrf_horserider") |         self.assertTableExists("test_rmwsrf_horserider") | ||||||
| @@ -642,7 +644,7 @@ class OperationTests(OperationTestBase): | |||||||
|             self.assertFKNotExists("test_rmwsrf_horserider", ["friend_id"], ("test_rmwsrf_rider", "id")) |             self.assertFKNotExists("test_rmwsrf_horserider", ["friend_id"], ("test_rmwsrf_rider", "id")) | ||||||
|             self.assertFKExists("test_rmwsrf_horserider", ["friend_id"], ("test_rmwsrf_horserider", "id")) |             self.assertFKExists("test_rmwsrf_horserider", ["friend_id"], ("test_rmwsrf_horserider", "id")) | ||||||
|         # And test reversal |         # And test reversal | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=atomic_rename) as editor: | ||||||
|             operation.database_backwards("test_rmwsrf", editor, new_state, project_state) |             operation.database_backwards("test_rmwsrf", editor, new_state, project_state) | ||||||
|         self.assertTableExists("test_rmwsrf_rider") |         self.assertTableExists("test_rmwsrf_rider") | ||||||
|         self.assertTableNotExists("test_rmwsrf_horserider") |         self.assertTableNotExists("test_rmwsrf_horserider") | ||||||
| @@ -675,7 +677,7 @@ class OperationTests(OperationTestBase): | |||||||
|             # and the foreign key on rider points to pony, not shetland pony |             # and the foreign key on rider points to pony, not shetland pony | ||||||
|             self.assertFKExists("test_rmwsc_rider", ["pony_id"], ("test_rmwsc_pony", "id")) |             self.assertFKExists("test_rmwsc_rider", ["pony_id"], ("test_rmwsc_pony", "id")) | ||||||
|             self.assertFKNotExists("test_rmwsc_rider", ["pony_id"], ("test_rmwsc_shetlandpony", "id")) |             self.assertFKNotExists("test_rmwsc_rider", ["pony_id"], ("test_rmwsc_shetlandpony", "id")) | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=connection.features.supports_atomic_references_rename) as editor: | ||||||
|             operation.database_forwards("test_rmwsc", editor, project_state, new_state) |             operation.database_forwards("test_rmwsc", editor, project_state, new_state) | ||||||
|         # Now we have a little horse table, not shetland pony |         # Now we have a little horse table, not shetland pony | ||||||
|         self.assertTableNotExists("test_rmwsc_shetlandpony") |         self.assertTableNotExists("test_rmwsc_shetlandpony") | ||||||
| @@ -696,7 +698,7 @@ class OperationTests(OperationTestBase): | |||||||
|         ]) |         ]) | ||||||
|         project_state = self.apply_operations(app_label, project_state, operations=[ |         project_state = self.apply_operations(app_label, project_state, operations=[ | ||||||
|             migrations.RenameModel("ReflexivePony", "ReflexivePony2"), |             migrations.RenameModel("ReflexivePony", "ReflexivePony2"), | ||||||
|         ]) |         ], atomic=connection.features.supports_atomic_references_rename) | ||||||
|         Pony = project_state.apps.get_model(app_label, "ReflexivePony2") |         Pony = project_state.apps.get_model(app_label, "ReflexivePony2") | ||||||
|         pony = Pony.objects.create() |         pony = Pony.objects.create() | ||||||
|         pony.ponies.add(pony) |         pony.ponies.add(pony) | ||||||
| @@ -749,7 +751,7 @@ class OperationTests(OperationTestBase): | |||||||
|  |  | ||||||
|         project_state = self.apply_operations(app_label, project_state, operations=[ |         project_state = self.apply_operations(app_label, project_state, operations=[ | ||||||
|             migrations.RenameModel("Rider", "Rider2"), |             migrations.RenameModel("Rider", "Rider2"), | ||||||
|         ]) |         ], atomic=connection.features.supports_atomic_references_rename) | ||||||
|         Pony = project_state.apps.get_model(app_label, "Pony") |         Pony = project_state.apps.get_model(app_label, "Pony") | ||||||
|         Rider = project_state.apps.get_model(app_label, "Rider2") |         Rider = project_state.apps.get_model(app_label, "Rider2") | ||||||
|         pony = Pony.objects.create() |         pony = Pony.objects.create() | ||||||
| @@ -1397,7 +1399,7 @@ class OperationTests(OperationTestBase): | |||||||
|         project_state = self.apply_operations(app_label, project_state, operations=[ |         project_state = self.apply_operations(app_label, project_state, operations=[ | ||||||
|             migrations.RenameField('Rider', 'id', 'id2'), |             migrations.RenameField('Rider', 'id', 'id2'), | ||||||
|             migrations.AlterField('Pony', 'id', models.CharField(primary_key=True, max_length=99)), |             migrations.AlterField('Pony', 'id', models.CharField(primary_key=True, max_length=99)), | ||||||
|         ]) |         ], atomic=connection.features.supports_atomic_references_rename) | ||||||
|  |  | ||||||
|     def test_rename_field(self): |     def test_rename_field(self): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -173,7 +173,7 @@ class SchemaTests(TransactionTestCase): | |||||||
|         index_orders = constraints[index]['orders'] |         index_orders = constraints[index]['orders'] | ||||||
|         self.assertTrue(all(val == expected for val, expected in zip(index_orders, order))) |         self.assertTrue(all(val == expected for val, expected in zip(index_orders, order))) | ||||||
|  |  | ||||||
|     def assertForeignKeyExists(self, model, column, expected_fk_table): |     def assertForeignKeyExists(self, model, column, expected_fk_table, field='id'): | ||||||
|         """ |         """ | ||||||
|         Fail if the FK constraint on `model.Meta.db_table`.`column` to |         Fail if the FK constraint on `model.Meta.db_table`.`column` to | ||||||
|         `expected_fk_table`.id doesn't exist. |         `expected_fk_table`.id doesn't exist. | ||||||
| @@ -184,7 +184,7 @@ class SchemaTests(TransactionTestCase): | |||||||
|             if details['columns'] == [column] and details['foreign_key']: |             if details['columns'] == [column] and details['foreign_key']: | ||||||
|                 constraint_fk = details['foreign_key'] |                 constraint_fk = details['foreign_key'] | ||||||
|                 break |                 break | ||||||
|         self.assertEqual(constraint_fk, (expected_fk_table, 'id')) |         self.assertEqual(constraint_fk, (expected_fk_table, field)) | ||||||
|  |  | ||||||
|     def assertForeignKeyNotExists(self, model, column, expected_fk_table): |     def assertForeignKeyNotExists(self, model, column, expected_fk_table): | ||||||
|         with self.assertRaises(AssertionError): |         with self.assertRaises(AssertionError): | ||||||
| @@ -1147,6 +1147,30 @@ class SchemaTests(TransactionTestCase): | |||||||
|         self.assertEqual(columns['display_name'][0], "CharField") |         self.assertEqual(columns['display_name'][0], "CharField") | ||||||
|         self.assertNotIn("name", columns) |         self.assertNotIn("name", columns) | ||||||
|  |  | ||||||
|  |     @isolate_apps('schema') | ||||||
|  |     def test_rename_referenced_field(self): | ||||||
|  |         class Author(Model): | ||||||
|  |             name = CharField(max_length=255, unique=True) | ||||||
|  |  | ||||||
|  |             class Meta: | ||||||
|  |                 app_label = 'schema' | ||||||
|  |  | ||||||
|  |         class Book(Model): | ||||||
|  |             author = ForeignKey(Author, CASCADE, to_field='name') | ||||||
|  |  | ||||||
|  |             class Meta: | ||||||
|  |                 app_label = 'schema' | ||||||
|  |  | ||||||
|  |         with connection.schema_editor() as editor: | ||||||
|  |             editor.create_model(Author) | ||||||
|  |             editor.create_model(Book) | ||||||
|  |         new_field = CharField(max_length=255, unique=True) | ||||||
|  |         new_field.set_attributes_from_name('renamed') | ||||||
|  |         with connection.schema_editor(atomic=connection.features.supports_atomic_references_rename) as editor: | ||||||
|  |             editor.alter_field(Author, Author._meta.get_field('name'), new_field) | ||||||
|  |         # Ensure the foreign key reference was updated. | ||||||
|  |         self.assertForeignKeyExists(Book, 'author_id', 'schema_author', 'renamed') | ||||||
|  |  | ||||||
|     @skipIfDBFeature('interprets_empty_strings_as_nulls') |     @skipIfDBFeature('interprets_empty_strings_as_nulls') | ||||||
|     def test_rename_keep_null_status(self): |     def test_rename_keep_null_status(self): | ||||||
|         """ |         """ | ||||||
| @@ -1625,25 +1649,41 @@ class SchemaTests(TransactionTestCase): | |||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @isolate_apps('schema') | ||||||
|     def test_db_table(self): |     def test_db_table(self): | ||||||
|         """ |         """ | ||||||
|         Tests renaming of the table |         Tests renaming of the table | ||||||
|         """ |         """ | ||||||
|         # Create the table |         class Author(Model): | ||||||
|  |             name = CharField(max_length=255) | ||||||
|  |  | ||||||
|  |             class Meta: | ||||||
|  |                 app_label = 'schema' | ||||||
|  |  | ||||||
|  |         class Book(Model): | ||||||
|  |             author = ForeignKey(Author, CASCADE) | ||||||
|  |  | ||||||
|  |             class Meta: | ||||||
|  |                 app_label = 'schema' | ||||||
|  |  | ||||||
|  |         # Create the table and one referring it. | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor() as editor: | ||||||
|             editor.create_model(Author) |             editor.create_model(Author) | ||||||
|  |             editor.create_model(Book) | ||||||
|         # Ensure the table is there to begin with |         # Ensure the table is there to begin with | ||||||
|         columns = self.column_classes(Author) |         columns = self.column_classes(Author) | ||||||
|         self.assertEqual(columns['name'][0], "CharField") |         self.assertEqual(columns['name'][0], "CharField") | ||||||
|         # Alter the table |         # Alter the table | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=connection.features.supports_atomic_references_rename) as editor: | ||||||
|             editor.alter_db_table(Author, "schema_author", "schema_otherauthor") |             editor.alter_db_table(Author, "schema_author", "schema_otherauthor") | ||||||
|         # Ensure the table is there afterwards |         # Ensure the table is there afterwards | ||||||
|         Author._meta.db_table = "schema_otherauthor" |         Author._meta.db_table = "schema_otherauthor" | ||||||
|         columns = self.column_classes(Author) |         columns = self.column_classes(Author) | ||||||
|         self.assertEqual(columns['name'][0], "CharField") |         self.assertEqual(columns['name'][0], "CharField") | ||||||
|  |         # Ensure the foreign key reference was updated | ||||||
|  |         self.assertForeignKeyExists(Book, "author_id", "schema_otherauthor") | ||||||
|         # Alter the table again |         # Alter the table again | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor(atomic=connection.features.supports_atomic_references_rename) as editor: | ||||||
|             editor.alter_db_table(Author, "schema_otherauthor", "schema_author") |             editor.alter_db_table(Author, "schema_otherauthor", "schema_author") | ||||||
|         # Ensure the table is still there |         # Ensure the table is still there | ||||||
|         Author._meta.db_table = "schema_author" |         Author._meta.db_table = "schema_author" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user