mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	[1.7.x] Fixed #24110 -- Rewrote migration unapply to preserve intermediate states
Backport offdc2cc9487andbe158e3625from master
This commit is contained in:
		| @@ -115,29 +115,39 @@ class Migration(object): | |||||||
|         Takes a project_state representing all migrations prior to this one |         Takes a project_state representing all migrations prior to this one | ||||||
|         and a schema_editor for a live database and applies the migration |         and a schema_editor for a live database and applies the migration | ||||||
|         in a reverse order. |         in a reverse order. | ||||||
|  |  | ||||||
|  |         The backwards migration process consists of two phases: | ||||||
|  |  | ||||||
|  |         1. The intermediate states from right before the first until right | ||||||
|  |            after the last operation inside this migration are preserved. | ||||||
|  |         2. The operations are applied in reverse order using the states | ||||||
|  |            recorded in step 1. | ||||||
|         """ |         """ | ||||||
|         # We need to pre-calculate the stack of project states |         # Construct all the intermediate states we need for a reverse migration | ||||||
|         to_run = [] |         to_run = [] | ||||||
|  |         new_state = project_state | ||||||
|  |         # Phase 1 | ||||||
|         for operation in self.operations: |         for operation in self.operations: | ||||||
|             # If this operation cannot be represented as SQL, place a comment |             # If it's irreversible, error out | ||||||
|             # there instead |             if not operation.reversible: | ||||||
|             if collect_sql and not operation.reduces_to_sql: |                 raise Migration.IrreversibleError("Operation %s in %s is not reversible" % (operation, self)) | ||||||
|  |             # Preserve new state from previous run to not tamper the same state | ||||||
|  |             # over all operations | ||||||
|  |             new_state = new_state.clone() | ||||||
|  |             old_state = new_state.clone() | ||||||
|  |             operation.state_forwards(self.app_label, new_state) | ||||||
|  |             to_run.insert(0, (operation, old_state, new_state)) | ||||||
|  |  | ||||||
|  |         # Phase 2 | ||||||
|  |         for operation, to_state, from_state in to_run: | ||||||
|  |             if collect_sql: | ||||||
|  |                 if not operation.reduces_to_sql: | ||||||
|                     schema_editor.collected_sql.append("--") |                     schema_editor.collected_sql.append("--") | ||||||
|                     schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE " |                     schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE " | ||||||
|                                                        "WRITTEN AS SQL:") |                                                        "WRITTEN AS SQL:") | ||||||
|                     schema_editor.collected_sql.append("-- %s" % operation.describe()) |                     schema_editor.collected_sql.append("-- %s" % operation.describe()) | ||||||
|                     schema_editor.collected_sql.append("--") |                     schema_editor.collected_sql.append("--") | ||||||
|                     continue |                     continue | ||||||
|             # If it's irreversible, error out |  | ||||||
|             if not operation.reversible: |  | ||||||
|                 raise Migration.IrreversibleError("Operation %s in %s is not reversible" % (operation, self)) |  | ||||||
|             new_state = project_state.clone() |  | ||||||
|             operation.state_forwards(self.app_label, new_state) |  | ||||||
|             to_run.append((operation, project_state, new_state)) |  | ||||||
|             project_state = new_state |  | ||||||
|         # Now run them in reverse |  | ||||||
|         to_run.reverse() |  | ||||||
|         for operation, to_state, from_state in to_run: |  | ||||||
|             if not schema_editor.connection.features.can_rollback_ddl and operation.atomic: |             if not schema_editor.connection.features.can_rollback_ddl and operation.atomic: | ||||||
|                 # We're forcing a transaction on a non-transactional-DDL backend |                 # We're forcing a transaction on a non-transactional-DDL backend | ||||||
|                 with atomic(schema_editor.connection.alias): |                 with atomic(schema_editor.connection.alias): | ||||||
|   | |||||||
| @@ -26,3 +26,6 @@ Bugfixes | |||||||
|   (:ticket:`24097`). |   (:ticket:`24097`). | ||||||
|  |  | ||||||
| * Added correct formats for Greek (``el``) (:ticket:`23967`). | * Added correct formats for Greek (``el``) (:ticket:`23967`). | ||||||
|  |  | ||||||
|  | * Fixed a migration crash when unapplying a migration where multiple operations | ||||||
|  |   interact with the same model (:ticket:`24110`). | ||||||
|   | |||||||
| @@ -413,27 +413,33 @@ class OperationTests(OperationTestBase): | |||||||
|         # Test the state alteration |         # Test the state alteration | ||||||
|         operation = migrations.RenameModel("Pony", "Horse") |         operation = migrations.RenameModel("Pony", "Horse") | ||||||
|         self.assertEqual(operation.describe(), "Rename model Pony to Horse") |         self.assertEqual(operation.describe(), "Rename model Pony to Horse") | ||||||
|         new_state = project_state.clone() |         # Test initial state and database | ||||||
|         operation.state_forwards("test_rnmo", new_state) |         self.assertIn(("test_rnmo", "pony"), project_state.models) | ||||||
|         self.assertNotIn(("test_rnmo", "pony"), new_state.models) |         self.assertNotIn(("test_rnmo", "horse"), project_state.models) | ||||||
|         self.assertIn(("test_rnmo", "horse"), new_state.models) |  | ||||||
|         # Remember, RenameModel also repoints all incoming FKs and M2Ms |  | ||||||
|         self.assertEqual("test_rnmo.Horse", new_state.models["test_rnmo", "rider"].fields[1][1].rel.to) |  | ||||||
|         # Test the database alteration |  | ||||||
|         self.assertTableExists("test_rnmo_pony") |         self.assertTableExists("test_rnmo_pony") | ||||||
|         self.assertTableNotExists("test_rnmo_horse") |         self.assertTableNotExists("test_rnmo_horse") | ||||||
|         if connection.features.supports_foreign_keys: |         if connection.features.supports_foreign_keys: | ||||||
|             self.assertFKExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_pony", "id")) |             self.assertFKExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_pony", "id")) | ||||||
|             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) |             self.assertFKNotExists("test_rnmo_rider", ["pony_id"], ("test_rnmo_horse", "id")) | ||||||
|         with connection.schema_editor() as editor: |         # Migrate forwards | ||||||
|             operation.database_forwards("test_rnmo", editor, project_state, new_state) |         new_state = project_state.clone() | ||||||
|  |         new_state = self.apply_operations("test_rnmo", new_state, [operation]) | ||||||
|  |         # Test new state and database | ||||||
|  |         self.assertNotIn(("test_rnmo", "pony"), new_state.models) | ||||||
|  |         self.assertIn(("test_rnmo", "horse"), new_state.models) | ||||||
|  |         # RenameModel also repoints all incoming FKs and M2Ms | ||||||
|  |         self.assertEqual("test_rnmo.Horse", new_state.models["test_rnmo", "rider"].fields[1][1].rel.to) | ||||||
|         self.assertTableNotExists("test_rnmo_pony") |         self.assertTableNotExists("test_rnmo_pony") | ||||||
|         self.assertTableExists("test_rnmo_horse") |         self.assertTableExists("test_rnmo_horse") | ||||||
|         if connection.features.supports_foreign_keys: |         if connection.features.supports_foreign_keys: | ||||||
|             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")) | ||||||
|         # And test reversal |         # Migrate backwards | ||||||
|         self.unapply_operations("test_rnmo", project_state, [operation]) |         original_state = self.unapply_operations("test_rnmo", project_state, [operation]) | ||||||
|  |         # Test original state and database | ||||||
|  |         self.assertIn(("test_rnmo", "pony"), original_state.models) | ||||||
|  |         self.assertNotIn(("test_rnmo", "horse"), original_state.models) | ||||||
|  |         self.assertEqual("Pony", original_state.models["test_rnmo", "rider"].fields[1][1].rel.to) | ||||||
|         self.assertTableExists("test_rnmo_pony") |         self.assertTableExists("test_rnmo_pony") | ||||||
|         self.assertTableNotExists("test_rnmo_horse") |         self.assertTableNotExists("test_rnmo_horse") | ||||||
|         if connection.features.supports_foreign_keys: |         if connection.features.supports_foreign_keys: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user