From fc40c46eb78f5659e77ed37aa0bc70aef6cfd48e Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 4 Aug 2025 22:32:05 +0000
Subject: [PATCH] Fix assertion failure in postgresGetForeignJoinPaths().

---
 contrib/postgres_fdw/Makefile                 |  4 +
 contrib/postgres_fdw/postgres_fdw.c           | 16 +++-
 contrib/postgres_fdw/t/002_foreign_recheck.pl | 94 +++++++++++++++++++
 src/backend/access/heap/heapam_handler.c      |  3 +
 4 files changed, 114 insertions(+), 3 deletions(-)
 create mode 100644 contrib/postgres_fdw/t/002_foreign_recheck.pl

diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile
index adfbd2ef758..8b88e42bb76 100644
--- a/contrib/postgres_fdw/Makefile
+++ b/contrib/postgres_fdw/Makefile
@@ -19,6 +19,10 @@ DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.s
 REGRESS = postgres_fdw query_cancel
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 456b267f70b..23c1b956048 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -6276,6 +6276,7 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
 	Cost		total_cost;
 	Path	   *epq_path;		/* Path to create plan to be executed when
 								 * EvalPlanQual gets triggered. */
+	bool		need_epq = false;
 
 	/*
 	 * Skip if this join combination has been considered already.
@@ -6313,9 +6314,18 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
 	 * calling foreign_join_ok(), since that function updates fpinfo and marks
 	 * it as pushable if the join is found to be pushable.
 	 */
-	if (root->parse->commandType == CMD_DELETE ||
-		root->parse->commandType == CMD_UPDATE ||
-		root->rowMarks)
+	for (PlannerInfo *proot = root; proot != NULL; proot = proot->parent_root)
+	{
+		if (proot->parse->commandType == CMD_DELETE ||
+			proot->parse->commandType == CMD_UPDATE ||
+			proot->rowMarks)
+		{
+			need_epq = true;
+			break;
+		}
+	}
+
+	if (need_epq)
 	{
 		epq_path = GetExistingLocalJoinPath(joinrel);
 		if (!epq_path)
diff --git a/contrib/postgres_fdw/t/002_foreign_recheck.pl b/contrib/postgres_fdw/t/002_foreign_recheck.pl
new file mode 100644
index 00000000000..943c65b3bca
--- /dev/null
+++ b/contrib/postgres_fdw/t/002_foreign_recheck.pl
@@ -0,0 +1,94 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node1 = PostgreSQL::Test::Cluster->new('node1');
+my $node2 = PostgreSQL::Test::Cluster->new('node2');
+$node1->init;
+$node2->init;
+$node1->start;
+$node2->start;
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node1->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node1->safe_psql('postgres',
+		  qq[
+create extension postgres_fdw;
+create extension injection_points;
+]);
+
+my $host = $node2->host;
+my $port = $node2->port;
+$node1->safe_psql('postgres',
+		 qq[
+create server node2 foreign data wrapper postgres_fdw options (
+    host '$host', port '$port', dbname 'postgres');
+create user mapping for public server node2;
+
+create table a (i int primary key);
+create foreign table b (i int) server node2;
+create foreign table c (i int) server node2;
+]);
+
+$node2->safe_psql('postgres',
+		  qq[
+create table b (i int);
+create table c (i int);
+]);
+
+$node1->safe_psql('postgres',
+		  qq[
+insert into a values (1);
+insert into b values (1);
+insert into c values (1);
+]);
+
+my $psql_session = $node1->background_psql('postgres');
+
+$psql_session->query_safe(qq[
+    select injection_points_set_local();
+    select injection_points_attach('heapam_lock_tuple-before-lock', 'wait');
+]);
+
+# Execute the query and stops before locking the result tuple.
+$psql_session->query_until(
+    qr/lock_tuple/, q(
+\echo lock_tuple
+select
+   (select a.i from b, c where a.i = b.i and b.i = c.i)
+from a
+for update;
+));
+$node1->wait_for_event('client backend', 'heapam_lock_tuple-before-lock');
+
+# Update the tupel concurrently
+$node1->safe_psql('postgres',
+		  qq[update a set i = i + 1]);
+
+# Wake up the FOR UPDATE query. The query detects the concurrent update and
+# performs recheck using EvalPlanQual().
+$node1->safe_psql('postgres',
+		 qq[select injection_points_wakeup('heapam_lock_tuple-before-lock')]);
+
+ok($psql_session->quit);
+
+$node1->stop;
+$node2->stop;
+done_testing();
+
+
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index cb4bc35c93e..45b1cafc01d 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -44,6 +44,7 @@
 #include "storage/procarray.h"
 #include "storage/smgr.h"
 #include "utils/builtins.h"
+#include "utils/injection_point.h"
 #include "utils/rel.h"
 
 static void reform_and_rewrite_tuple(HeapTuple tuple,
@@ -375,6 +376,8 @@ heapam_tuple_lock(Relation relation, ItemPointer tid, Snapshot snapshot,
 
 	Assert(TTS_IS_BUFFERTUPLE(slot));
 
+	INJECTION_POINT("heapam_lock_tuple-before-lock", NULL);
+
 tuple_lock_retry:
 	tuple->t_self = *tid;
 	result = heap_lock_tuple(relation, tuple, cid, mode, wait_policy,
-- 
2.47.3

