From c5c14c328087ab7d52d6e3430744580be37b6dc8 Mon Sep 17 00:00:00 2001
From: David Rowley <dgrowley@gmail.com>
Date: Mon, 24 Aug 2026 16:53:15 +1200
Subject: [PATCH v16] Close relations opened specifically for AFTER triggers

39dcfda2d fixed an incorrect reuse of ResultRelInfos for AFTER triggers
when the ResultRelInfo needed to have a different ri_RootResultRelInfo.
That caused an issue in logical replication apply workers as
finish_edata() neglects to call ExecCloseResultRelations() and instead
relies on ExecCleanupTupleRouting() to close relations opened during
partitioning's tuple routing.  Since 39dcfda2d, because we may have done
some additional table_opens() calls due to having to create an additional
ResultRelInfo because of requirements to have a different
ri_RootResultRelInfo, we should now be explicitly closing any relations
opened on ResultRelInfos in EState's es_trig_target_relations.

Author: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
Author: David Rowley <dgrowleyml@gmail.com>
Discussion: https://postgr.es/m/OS9PR01MB121491E7E05950D108AF9A6D8F5A72@OS9PR01MB12149.jpnprd01.prod.outlook.com
Backpatch-through: 15
---
 src/backend/executor/execMain.c          | 16 ++++++++++++++++
 src/backend/replication/logical/worker.c | 12 ++++++++----
 src/include/executor/executor.h          |  1 +
 src/test/subscription/t/013_partition.pl |  7 +++++++
 4 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 6a60fb33377..c3ca801f2a8 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1621,6 +1621,22 @@ ExecCloseResultRelations(EState *estate)
 		}
 	}
 
+	/*
+	 * Now close any relations that we opened for trigger target
+	 * ResultRelInfos.
+	 */
+	ExecCloseTrigTargetRelations(estate);
+}
+
+/*
+ * Close any relations that have been opened for ResultRelInfos opened
+ * specifically for trigger target relations.
+ */
+void
+ExecCloseTrigTargetRelations(EState *estate)
+{
+	ListCell   *l;
+
 	/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
 	foreach(l, estate->es_trig_target_relations)
 	{
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index c3dd1537185..76367458768 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -731,12 +731,16 @@ finish_edata(ApplyExecutionData *edata)
 		ExecCleanupTupleRouting(edata->mtstate, edata->proute);
 
 	/*
-	 * Cleanup.  It might seem that we should call ExecCloseResultRelations()
-	 * here, but we intentionally don't.  It would close the rel we added to
+	 * Close relations opened specifically for trigger targets.  It might seem
+	 * that we should call ExecCloseResultRelations() here, but we
+	 * intentionally don't as that would close the rel we added to
 	 * es_opened_result_relations above, which is wrong because we took no
-	 * corresponding refcount.  We rely on ExecCleanupTupleRouting() to close
-	 * any other relations opened during execution.
+	 * corresponding refcount.  ExecCleanupTupleRouting() closes relations
+	 * opened for tuple routing, while ExecCloseTrigTargetRelations() closes
+	 * any relations we opened for AFTER triggers.
 	 */
+	ExecCloseTrigTargetRelations(estate);
+
 	ExecResetTupleTable(estate->es_tupleTable, false);
 	FreeExecutorState(estate);
 	pfree(edata);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index baef7e031ee..deba7a64af0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -586,6 +586,7 @@ extern Relation ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags
 extern void ExecInitRangeTable(EState *estate, List *rangeTable, List *permInfos);
 extern void ExecCloseRangeTableRelations(EState *estate);
 extern void ExecCloseResultRelations(EState *estate);
+extern void ExecCloseTrigTargetRelations(EState *estate);
 
 static inline RangeTblEntry *
 exec_rt_fetch(Index rti, EState *estate)
diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl
index 275fb3b5257..912ca3336bd 100644
--- a/src/test/subscription/t/013_partition.pl
+++ b/src/test/subscription/t/013_partition.pl
@@ -886,4 +886,11 @@ $result = $node_subscriber2->safe_psql('postgres',
 	"SELECT a, b, c FROM tab5_1 ORDER BY 1");
 is($result, qq(4||1), 'updates of tab5 replicated correctly');
 
+# Validate we didn't neglect to cleanup any resources on either subscriber.
+foreach my $node ($node_subscriber1, $node_subscriber2)
+{
+	ok(!$node->log_contains(qr/relcache reference leak/),
+		'no unclosed resources on ' . $node->name);
+}
+
 done_testing();
-- 
2.53.0

