@@ -2588,6 +2588,160 @@ def test_operation(self):
25882588 AND convalidated IS TRUE;
25892589 """ )
25902590
2591+ @pytest .mark .django_db (transaction = True )
2592+ def test_operation_when_already_removed_from_state (self ):
2593+ with connection .cursor () as cursor :
2594+ # Set the lock_timeout to check it has been returned to
2595+ # its original value once the fk index creation is completed by
2596+ # the reverse operation.
2597+ cursor .execute (_SET_LOCK_TIMEOUT )
2598+
2599+ project_state = ProjectState ()
2600+ project_state .add_model (ModelState .from_model (IntModel ))
2601+ project_state .add_model (ModelState .from_model (ModelWithForeignKey ))
2602+ new_state = project_state .clone ()
2603+ operation = operations .SaferRemoveFieldForeignKey (
2604+ model_name = "modelwithforeignkey" ,
2605+ name = "fk" ,
2606+ )
2607+
2608+ assert operation .describe () == (
2609+ "Remove field fk from modelwithforeignkey. Note: Using "
2610+ "django_pg_migration_tools SaferRemoveFieldForeignKey operation."
2611+ )
2612+
2613+ operation .state_forwards (self .app_label , new_state )
2614+
2615+ # Do database only operation - has already been removed from state
2616+ newer_state = new_state .clone ()
2617+ with connection .schema_editor (atomic = False , collect_sql = False ) as editor :
2618+ with utils .CaptureQueriesContext (connection ) as queries :
2619+ operation .database_forwards (
2620+ self .app_label , editor , from_state = new_state , to_state = newer_state
2621+ )
2622+
2623+ assert len (queries ) == 2
2624+
2625+ assert queries [0 ]["sql" ] == dedent (
2626+ """
2627+ SELECT 1
2628+ FROM pg_catalog.pg_attribute
2629+ WHERE
2630+ attrelid = 'example_app_modelwithforeignkey'::regclass
2631+ AND attname = 'fk_id';
2632+ """
2633+ )
2634+ assert queries [1 ]["sql" ] == dedent (
2635+ """
2636+ ALTER TABLE "example_app_modelwithforeignkey"
2637+ DROP COLUMN "fk_id";
2638+ """
2639+ )
2640+
2641+ with connection .schema_editor (atomic = False , collect_sql = False ) as editor :
2642+ with utils .CaptureQueriesContext (connection ) as reverse_queries :
2643+ operation .database_backwards (
2644+ self .app_label , editor , from_state = new_state , to_state = project_state
2645+ )
2646+
2647+ assert len (reverse_queries ) == 9
2648+
2649+ assert reverse_queries [0 ]["sql" ] == dedent (
2650+ """
2651+ SELECT 1
2652+ FROM pg_catalog.pg_attribute
2653+ WHERE
2654+ attrelid = 'example_app_modelwithforeignkey'::regclass
2655+ AND attname = 'fk_id';
2656+ """
2657+ )
2658+ assert reverse_queries [1 ]["sql" ] == dedent (
2659+ """
2660+ ALTER TABLE "example_app_modelwithforeignkey"
2661+ ADD COLUMN IF NOT EXISTS "fk_id"
2662+ integer NULL;
2663+ """
2664+ )
2665+ assert reverse_queries [2 ]["sql" ] == "SHOW lock_timeout;"
2666+ assert reverse_queries [3 ]["sql" ] == "SET lock_timeout = '0';"
2667+ assert reverse_queries [4 ]["sql" ] == dedent (
2668+ """
2669+ SELECT relname
2670+ FROM pg_class, pg_index
2671+ WHERE (
2672+ pg_index.indisvalid = false
2673+ AND pg_index.indexrelid = pg_class.oid
2674+ AND relname = 'modelwithforeignkey_fk_id_idx'
2675+ );
2676+ """
2677+ )
2678+ assert (
2679+ reverse_queries [5 ]["sql" ]
2680+ == 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "modelwithforeignkey_fk_id_idx" ON "example_app_modelwithforeignkey" ("fk_id");'
2681+ )
2682+ assert reverse_queries [6 ]["sql" ] == "SET lock_timeout = '1s';"
2683+ assert reverse_queries [7 ]["sql" ] == dedent (
2684+ """
2685+ ALTER TABLE "example_app_modelwithforeignkey"
2686+ ADD CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk" FOREIGN KEY ("fk_id")
2687+ REFERENCES "example_app_intmodel" ("id")
2688+ DEFERRABLE INITIALLY DEFERRED
2689+ NOT VALID;
2690+ """
2691+ )
2692+ assert reverse_queries [8 ]["sql" ] == dedent (
2693+ """
2694+ ALTER TABLE "example_app_modelwithforeignkey"
2695+ VALIDATE CONSTRAINT "example_app_modelwithforeignkey_fk_id_fk";
2696+ """
2697+ )
2698+
2699+ # Reversing again does nothing apart from checking that the FK is
2700+ # already there and the index/constraint are all good to go.
2701+ # This proves the OP is idempotent.
2702+ with connection .schema_editor (atomic = False , collect_sql = False ) as editor :
2703+ with utils .CaptureQueriesContext (connection ) as second_reverse_queries :
2704+ operation .database_backwards (
2705+ self .app_label , editor , from_state = new_state , to_state = project_state
2706+ )
2707+ assert len (second_reverse_queries ) == 4
2708+ assert second_reverse_queries [0 ]["sql" ] == dedent (
2709+ """
2710+ SELECT 1
2711+ FROM pg_catalog.pg_attribute
2712+ WHERE
2713+ attrelid = 'example_app_modelwithforeignkey'::regclass
2714+ AND attname = 'fk_id';
2715+ """
2716+ )
2717+ assert second_reverse_queries [1 ]["sql" ] == dedent (
2718+ """
2719+ SELECT 1
2720+ FROM pg_class, pg_index
2721+ WHERE (
2722+ pg_index.indisvalid = true
2723+ AND pg_index.indexrelid = pg_class.oid
2724+ AND relname = 'modelwithforeignkey_fk_id_idx'
2725+ );
2726+ """
2727+ )
2728+ assert second_reverse_queries [2 ]["sql" ] == dedent (
2729+ """
2730+ SELECT conname
2731+ FROM pg_catalog.pg_constraint
2732+ WHERE conname = 'example_app_modelwithforeignkey_fk_id_fk';
2733+ """
2734+ )
2735+ assert second_reverse_queries [3 ]["sql" ] == dedent (
2736+ """
2737+ SELECT 1
2738+ FROM pg_catalog.pg_constraint
2739+ WHERE
2740+ conname = 'example_app_modelwithforeignkey_fk_id_fk'
2741+ AND convalidated IS TRUE;
2742+ """
2743+ )
2744+
25912745 @pytest .mark .django_db (transaction = True )
25922746 def test_when_column_not_null (self ):
25932747 with connection .cursor () as cursor :
0 commit comments