From 695a50438d11c8ce7ad0e2b26b34824ce7e11459 Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Fri, 10 Jan 2025 11:20:05 +1300 Subject: [PATCH 1/6] Remove duplicate word --- tests/django_pg_migration_tools/test_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/django_pg_migration_tools/test_operations.py b/tests/django_pg_migration_tools/test_operations.py index 878ab72..c3bc459 100644 --- a/tests/django_pg_migration_tools/test_operations.py +++ b/tests/django_pg_migration_tools/test_operations.py @@ -2024,7 +2024,7 @@ def test_when_valid_constraint_and_alter_table_already_performed(self): """) -class TestSaferSaferAddFieldForeignKey: +class TestSaferAddFieldForeignKey: app_label = "example_app" @pytest.mark.django_db From 3bdf332001b34513219d9ab351c17e4b0ee1fcd7 Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Fri, 10 Jan 2025 15:04:00 +1300 Subject: [PATCH 2/6] Change collect_default for removing fk This will allow us to show the proper queries when running on collect-only mode (like when running sqlmigrate). This change will be more useful in a future commit when adding the SaferRemoveFieldForeignKey operation. --- src/django_pg_migration_tools/operations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/django_pg_migration_tools/operations.py b/src/django_pg_migration_tools/operations.py index 95e2a3c..1a0ddb6 100644 --- a/src/django_pg_migration_tools/operations.py +++ b/src/django_pg_migration_tools/operations.py @@ -1262,12 +1262,12 @@ def drop_fk_field(self) -> None: ): return - if not self._column_exists(): + if not self._column_exists(collect_default=True): return self._alter_table_drop_column() - def _column_exists(self) -> bool: + def _column_exists(self, collect_default: bool = False) -> bool: return _run_introspection_query( self.schema_editor, psycopg_sql.SQL(ColumnQueries.CHECK_COLUMN_EXISTS) @@ -1276,6 +1276,7 @@ def _column_exists(self) -> bool: column_name=psycopg_sql.Literal(self.column_name), ) .as_string(self.schema_editor.connection.connection), + collect_default=collect_default, ) def _get_remote_model(self) -> models.Model: From fe084e0542349e72a9e390c4e7b0b7ebf24e407f Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Fri, 10 Jan 2025 15:09:40 +1300 Subject: [PATCH 3/6] Fix test using wrong list of queries Funny enough the result was the same, though the test was using the wrong queries list. --- tests/django_pg_migration_tools/test_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/django_pg_migration_tools/test_operations.py b/tests/django_pg_migration_tools/test_operations.py index c3bc459..e72dc27 100644 --- a/tests/django_pg_migration_tools/test_operations.py +++ b/tests/django_pg_migration_tools/test_operations.py @@ -2194,7 +2194,7 @@ def test_operation(self): ) assert len(second_reverse_queries) == 1 - assert reverse_queries[0]["sql"] == dedent(""" + assert second_reverse_queries[0]["sql"] == dedent(""" SELECT 1 FROM pg_catalog.pg_attribute WHERE From 1a1c4675896cd5808a714bcf994a849eeba99797 Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Fri, 10 Jan 2025 15:12:30 +1300 Subject: [PATCH 4/6] Introduce SaferRemoveFieldForeignKey This operation is the counterpoise of SaferAddFieldForeignKey. It allows to remove a foreign key more safely (idempotently), and also allows for a safer reverse operation (adding the FK field back again). --- CHANGELOG.md | 1 + src/django_pg_migration_tools/operations.py | 53 +++ .../test_operations.py | 328 ++++++++++++++++++ tests/example_app/models.py | 4 + 4 files changed, 386 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cdbc55..fde38ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to - Enhanced `SaferAddUniqueConstraint` to support a `UniqueConstraint` with the `deferrable` argument. +- A new operation to remove a foreign key field: `SaferRemoveFieldForeignKey`. ## [0.1.16] - 2025-01-08 diff --git a/src/django_pg_migration_tools/operations.py b/src/django_pg_migration_tools/operations.py index 1a0ddb6..2b8559c 100644 --- a/src/django_pg_migration_tools/operations.py +++ b/src/django_pg_migration_tools/operations.py @@ -1456,6 +1456,59 @@ def describe(self) -> str: ) +class SaferRemoveFieldForeignKey(operation_fields.RemoveField): + def database_forwards( + self, + app_label: str, + schema_editor: base_schema.BaseDatabaseSchemaEditor, + from_state: migrations.state.ProjectState, + to_state: migrations.state.ProjectState, + ) -> None: + field = from_state.apps.get_model(app_label, self.model_name)._meta.get_field( + self.name + ) + ForeignKeyManager( + app_label, + schema_editor, + from_state=from_state, + to_state=to_state, + model=to_state.apps.get_model(app_label, self.model_name), + model_name=self.model_name, + column_name=self.name, + field=field, + unique=False, + ).drop_fk_field() + + def database_backwards( + self, + app_label: str, + schema_editor: base_schema.BaseDatabaseSchemaEditor, + from_state: migrations.state.ProjectState, + to_state: migrations.state.ProjectState, + ) -> None: + field = to_state.apps.get_model(app_label, self.model_name)._meta.get_field( + self.name + ) + ForeignKeyManager( + app_label, + schema_editor, + from_state=from_state, + to_state=to_state, + model=to_state.apps.get_model(app_label, self.model_name), + model_name=self.model_name, + column_name=self.name, + field=field, + unique=False, + ).add_fk_field() + + def describe(self) -> str: + base = super().describe() + return ( + f"{base}. Note: Using django_pg_migration_tools " + f"SaferRemoveFieldForeignKey operation." + ) + + class SaferAddFieldOneToOne(operation_fields.AddField): """ Django's OneToOneField behaves the same way as a ForeignKey. Except that diff --git a/tests/django_pg_migration_tools/test_operations.py b/tests/django_pg_migration_tools/test_operations.py index e72dc27..5044c53 100644 --- a/tests/django_pg_migration_tools/test_operations.py +++ b/tests/django_pg_migration_tools/test_operations.py @@ -21,6 +21,7 @@ CharIDModel, CharModel, IntModel, + ModelWithForeignKey, NotNullIntFieldModel, NullIntFieldModel, ) @@ -2024,6 +2025,333 @@ def test_when_valid_constraint_and_alter_table_already_performed(self): """) +class TestSaferRemoveFieldForeignKey: + app_label = "example_app" + + @pytest.mark.django_db + def test_requires_atomic_false(self): + project_state = ProjectState() + project_state.add_model(ModelState.from_model(IntModel)) + project_state.add_model(ModelState.from_model(ModelWithForeignKey)) + new_state = project_state.clone() + operation = operations.SaferRemoveFieldForeignKey( + model_name="modelwithforeignkey", + name="fk", + ) + with pytest.raises(NotSupportedError): + with connection.schema_editor(atomic=True) as editor: + operation.database_forwards( + self.app_label, editor, project_state, new_state + ) + + @pytest.mark.django_db(transaction=True) + @override_settings(DATABASE_ROUTERS=[NeverAllow()]) + def test_when_not_allowed_to_migrate_by_the_router(self): + project_state = ProjectState() + project_state.add_model(ModelState.from_model(IntModel)) + project_state.add_model(ModelState.from_model(ModelWithForeignKey)) + new_state = project_state.clone() + operation = operations.SaferRemoveFieldForeignKey( + model_name="modelwithforeignkey", + name="fk", + ) + + operation.state_forwards(self.app_label, new_state) + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as queries: + operation.database_forwards( + self.app_label, editor, project_state, new_state + ) + # No queries have run, because the migration wasn't allowed to run by + # the router. + assert len(queries) == 0 + + # Try the same for the reverse operation: + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as queries: + operation.database_backwards( + self.app_label, editor, new_state, project_state + ) + + # No queries have run, because the migration wasn't allowed to run by + # the router. + assert len(queries) == 0 + + @pytest.mark.django_db(transaction=True) + def test_operation(self): + with connection.cursor() as cursor: + # Set the lock_timeout to check it has been returned to + # its original value once the fk index creation is completed by + # the reverse operation. + cursor.execute(_SET_LOCK_TIMEOUT) + + project_state = ProjectState() + project_state.add_model(ModelState.from_model(IntModel)) + project_state.add_model(ModelState.from_model(ModelWithForeignKey)) + new_state = project_state.clone() + operation = operations.SaferRemoveFieldForeignKey( + model_name="modelwithforeignkey", + name="fk", + ) + + assert operation.describe() == ( + "Remove field fk from modelwithforeignkey. Note: Using " + "django_pg_migration_tools SaferRemoveFieldForeignKey operation." + ) + + operation.state_forwards(self.app_label, new_state) + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as queries: + operation.database_forwards( + self.app_label, editor, project_state, new_state + ) + + assert len(queries) == 2 + + assert queries[0]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_attribute + WHERE + attrelid = 'example_app_modelwithforeignkey'::regclass + AND attname = 'fk_id'; + """) + assert queries[1]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + DROP COLUMN "fk_id"; + """) + + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as reverse_queries: + operation.database_backwards( + self.app_label, editor, new_state, project_state + ) + + assert len(reverse_queries) == 9 + + assert reverse_queries[0]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_attribute + WHERE + attrelid = 'example_app_modelwithforeignkey'::regclass + AND attname = 'fk_id'; + """) + assert reverse_queries[1]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD COLUMN IF NOT EXISTS "fk_id" + integer NULL; + """) + assert reverse_queries[2]["sql"] == "SHOW lock_timeout;" + assert reverse_queries[3]["sql"] == "SET lock_timeout = '0';" + assert reverse_queries[4]["sql"] == dedent(""" + SELECT relname + FROM pg_class, pg_index + WHERE ( + pg_index.indisvalid = false + AND pg_index.indexrelid = pg_class.oid + AND relname = 'modelwithforeignkey_fk_id_idx' + ); + """) + assert ( + reverse_queries[5]["sql"] + == 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "modelwithforeignkey_fk_id_idx" ON "example_app_modelwithforeignkey" ("fk_id");' + ) + assert reverse_queries[6]["sql"] == "SET lock_timeout = '1s';" + assert reverse_queries[7]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk" FOREIGN KEY ("fk_id") + REFERENCES "example_app_intmodel" ("id") + DEFERRABLE INITIALLY DEFERRED + NOT VALID; + """) + assert reverse_queries[8]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + VALIDATE CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk"; + """) + + # Reversing again does nothing apart from checking that the FK is + # already there and the index/constraint are all good to go. + # This proves the OP is idempotent. + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as second_reverse_queries: + operation.database_backwards( + self.app_label, editor, new_state, project_state + ) + assert len(second_reverse_queries) == 4 + assert second_reverse_queries[0]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_attribute + WHERE + attrelid = 'example_app_modelwithforeignkey'::regclass + AND attname = 'fk_id'; + """) + assert second_reverse_queries[1]["sql"] == dedent(""" + SELECT 1 + FROM pg_class, pg_index + WHERE ( + pg_index.indisvalid = true + AND pg_index.indexrelid = pg_class.oid + AND relname = 'modelwithforeignkey_fk_id_idx' + ); + """) + assert second_reverse_queries[2]["sql"] == dedent(""" + SELECT conname + FROM pg_catalog.pg_constraint + WHERE conname = 'example_app_modelwithforeignkey_fk_id_fk'; + """) + assert second_reverse_queries[3]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_constraint + WHERE + conname = 'example_app_modelwithforeignkey_fk_id_fk' + AND convalidated IS TRUE; + """) + + @pytest.mark.django_db(transaction=True) + def test_when_column_already_deleted(self): + with connection.cursor() as cursor: + cursor.execute(""" + ALTER TABLE "example_app_modelwithforeignkey" + DROP COLUMN "fk_id"; + """) + + project_state = ProjectState() + project_state.add_model(ModelState.from_model(IntModel)) + project_state.add_model(ModelState.from_model(ModelWithForeignKey)) + new_state = project_state.clone() + operation = operations.SaferRemoveFieldForeignKey( + model_name="modelwithforeignkey", + name="fk", + ) + + operation.state_forwards(self.app_label, new_state) + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as queries: + operation.database_forwards( + self.app_label, editor, project_state, new_state + ) + + assert len(queries) == 1 + + assert queries[0]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_attribute + WHERE + attrelid = 'example_app_modelwithforeignkey'::regclass + AND attname = 'fk_id'; + """) + + with connection.schema_editor(atomic=False, collect_sql=False) as editor: + with utils.CaptureQueriesContext(connection) as reverse_queries: + operation.database_backwards( + self.app_label, editor, new_state, project_state + ) + + assert len(reverse_queries) == 9 + + assert reverse_queries[0]["sql"] == dedent(""" + SELECT 1 + FROM pg_catalog.pg_attribute + WHERE + attrelid = 'example_app_modelwithforeignkey'::regclass + AND attname = 'fk_id'; + """) + assert reverse_queries[1]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD COLUMN IF NOT EXISTS "fk_id" + integer NULL; + """) + assert reverse_queries[2]["sql"] == "SHOW lock_timeout;" + assert reverse_queries[3]["sql"] == "SET lock_timeout = '0';" + assert reverse_queries[4]["sql"] == dedent(""" + SELECT relname + FROM pg_class, pg_index + WHERE ( + pg_index.indisvalid = false + AND pg_index.indexrelid = pg_class.oid + AND relname = 'modelwithforeignkey_fk_id_idx' + ); + """) + assert ( + reverse_queries[5]["sql"] + == 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "modelwithforeignkey_fk_id_idx" ON "example_app_modelwithforeignkey" ("fk_id");' + ) + assert reverse_queries[6]["sql"] == "SET lock_timeout = '0';" + assert reverse_queries[7]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk" FOREIGN KEY ("fk_id") + REFERENCES "example_app_intmodel" ("id") + DEFERRABLE INITIALLY DEFERRED + NOT VALID; + """) + assert reverse_queries[8]["sql"] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + VALIDATE CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk"; + """) + + @pytest.mark.django_db(transaction=True) + def test_when_only_collecting(self): + project_state = ProjectState() + project_state.add_model(ModelState.from_model(IntModel)) + project_state.add_model(ModelState.from_model(ModelWithForeignKey)) + new_state = project_state.clone() + operation = operations.SaferRemoveFieldForeignKey( + model_name="modelwithforeignkey", + name="fk", + ) + + assert operation.describe() == ( + "Remove field fk from modelwithforeignkey. Note: Using " + "django_pg_migration_tools SaferRemoveFieldForeignKey operation." + ) + + operation.state_forwards(self.app_label, new_state) + with connection.schema_editor(atomic=False, collect_sql=True) as editor: + with utils.CaptureQueriesContext(connection) as queries: + operation.database_forwards( + self.app_label, editor, project_state, new_state + ) + + assert len(queries) == 0 + assert len(editor.collected_sql) == 1 + + assert editor.collected_sql[0] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + DROP COLUMN "fk_id"; + """) + + with connection.schema_editor(atomic=False, collect_sql=True) as editor: + with utils.CaptureQueriesContext(connection) as reverse_queries: + operation.database_backwards( + self.app_label, editor, new_state, project_state + ) + + assert len(reverse_queries) == 0 + assert len(editor.collected_sql) == 6 + + assert editor.collected_sql[0] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD COLUMN IF NOT EXISTS "fk_id" + integer NULL; + """) + assert editor.collected_sql[1] == "SET lock_timeout = '0';" + assert ( + editor.collected_sql[2] + == 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "modelwithforeignkey_fk_id_idx" ON "example_app_modelwithforeignkey" ("fk_id");' + ) + assert editor.collected_sql[3] == "SET lock_timeout = '0';" + assert editor.collected_sql[4] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + ADD CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk" FOREIGN KEY ("fk_id") + REFERENCES "example_app_intmodel" ("id") + DEFERRABLE INITIALLY DEFERRED + NOT VALID; + """) + assert editor.collected_sql[5] == dedent(""" + ALTER TABLE "example_app_modelwithforeignkey" + VALIDATE CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk"; + """) + + class TestSaferAddFieldForeignKey: app_label = "example_app" diff --git a/tests/example_app/models.py b/tests/example_app/models.py index 7c44a2e..b35c23b 100644 --- a/tests/example_app/models.py +++ b/tests/example_app/models.py @@ -5,6 +5,10 @@ class IntModel(models.Model): int_field = models.IntegerField(default=0) +class ModelWithForeignKey(models.Model): + fk = models.ForeignKey(IntModel, null=True, on_delete=models.CASCADE) + + class CharModel(models.Model): char_field = models.CharField(default="char") From 34887c58913300136897a2941c4da3dda110b410 Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Fri, 10 Jan 2025 15:24:46 +1300 Subject: [PATCH 5/6] Add documentation for SaferRemoveFieldForeignKey --- docs/usage/operations.rst | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/usage/operations.rst b/docs/usage/operations.rst index 5ec4119..db3b134 100644 --- a/docs/usage/operations.rst +++ b/docs/usage/operations.rst @@ -517,6 +517,7 @@ Class Definitions ), ] +.. _safer_add_field_foreign_key: .. py:class:: SaferAddFieldForeignKey(model_name: str, name: str, field: models.ForeignKey) Provides a safer way to add a foreign key field to an existing model @@ -636,6 +637,73 @@ Class Definitions ), ] +.. py:class:: SaferRemoveFieldForeignKey(model_name: str, name: str) + + Provides a safer way to remove a foreign key field. + + :param model_name: Model name in lowercase without underscores. + :type model_name: str + :param name: The column name for the foreign key field to be deleted. + :type name: str + + **Why use this SaferRemoveFieldForeignKey operation?** + ------------------------------------------------------ + + The operation that Django provides (``RemoveField``) has the + following limitations: + + 1. The operation fails if the field has already been removed (not + idempotent). + 2. When reverting, the alter table statement provided by Django to recreate + the foreign key will block reads and writes on the table. + + This custom operation fixes those problems by: + + - Having a custom forward operation that will only attempt to drop the + foreign key field if the field exists. + - Having a custom backward operation that will add the foreign key back + without blocking any reads/writes. This is achieved through the same + strategy of :ref:`SaferAddFieldForeignKey `. + + How to use + ---------- + + 1. Remove the ForeignKey field from your model: + + .. code-block:: diff + + - bar = models.ForeignKey(Bar, null=True, on_delete=models.CASCADE) + + 2. Make the new migration: + + .. code-block:: bash + + ./manage.py makemigrations + + 3. The only changes you need to perform are: + + 1. Swap Django's ``RemoveField`` for this package's + ``SaferRemoveFieldForeignKey`` operation. + 2. Use a non-atomic migration. + + .. code-block:: diff + + + from django_pg_migration_tools import operations + from django.db import migrations + + + class Migration(migrations.Migration): + + atomic = False + + dependencies = [("myapp", "0042_dependency")] + + operations = [ + - migrations.RemoveField( + + operations.SaferRemoveFieldForeignKey( + model_name="mymodel", + name="bar", + ), + ] .. py:class:: SaferAddCheckConstraint(model_name: str, constraint: models.CheckConstraint) From 27c39e5cbbd9ba257126bf0aa4c24926d5c1597b Mon Sep 17 00:00:00 2001 From: Marcelo Fernandes Date: Tue, 14 Jan 2025 11:11:19 +1300 Subject: [PATCH 6/6] Consolidate argument order As per e2e14c3343851fb2f691b56dd93457efcac781e6 this should avoid some potentially mistaken copy/paste. --- .../test_operations.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/django_pg_migration_tools/test_operations.py b/tests/django_pg_migration_tools/test_operations.py index 5044c53..e5e8758 100644 --- a/tests/django_pg_migration_tools/test_operations.py +++ b/tests/django_pg_migration_tools/test_operations.py @@ -988,7 +988,7 @@ def test_when_deferred_set(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) with connection.cursor() as cursor: @@ -1043,7 +1043,7 @@ def test_when_deferred_set(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as reverse_queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) # 1. Check that the constraint is still there. @@ -2041,7 +2041,7 @@ def test_requires_atomic_false(self): with pytest.raises(NotSupportedError): with connection.schema_editor(atomic=True) as editor: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) @pytest.mark.django_db(transaction=True) @@ -2060,7 +2060,7 @@ def test_when_not_allowed_to_migrate_by_the_router(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) # No queries have run, because the migration wasn't allowed to run by # the router. @@ -2070,7 +2070,7 @@ def test_when_not_allowed_to_migrate_by_the_router(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) # No queries have run, because the migration wasn't allowed to run by @@ -2103,7 +2103,7 @@ def test_operation(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) assert len(queries) == 2 @@ -2123,7 +2123,7 @@ def test_operation(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as reverse_queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) assert len(reverse_queries) == 9 @@ -2174,7 +2174,7 @@ def test_operation(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as second_reverse_queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) assert len(second_reverse_queries) == 4 assert second_reverse_queries[0]["sql"] == dedent(""" @@ -2227,7 +2227,7 @@ def test_when_column_already_deleted(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) assert len(queries) == 1 @@ -2243,7 +2243,7 @@ def test_when_column_already_deleted(self): with connection.schema_editor(atomic=False, collect_sql=False) as editor: with utils.CaptureQueriesContext(connection) as reverse_queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) assert len(reverse_queries) == 9 @@ -2308,7 +2308,7 @@ def test_when_only_collecting(self): with connection.schema_editor(atomic=False, collect_sql=True) as editor: with utils.CaptureQueriesContext(connection) as queries: operation.database_forwards( - self.app_label, editor, project_state, new_state + self.app_label, editor, from_state=project_state, to_state=new_state ) assert len(queries) == 0 @@ -2322,7 +2322,7 @@ def test_when_only_collecting(self): with connection.schema_editor(atomic=False, collect_sql=True) as editor: with utils.CaptureQueriesContext(connection) as reverse_queries: operation.database_backwards( - self.app_label, editor, new_state, project_state + self.app_label, editor, from_state=new_state, to_state=project_state ) assert len(reverse_queries) == 0