Fix EvalPlanQual handling of foreign/custom joins in ExecScanFetch.
authorEtsuro Fujita <[email protected]>
Wed, 15 Oct 2025 08:15:00 +0000 (17:15 +0900)
committerEtsuro Fujita <[email protected]>
Wed, 15 Oct 2025 08:15:00 +0000 (17:15 +0900)
If inside an EPQ recheck, ExecScanFetch would run the recheck method
function for foreign/custom joins even if they aren't descendant nodes
in the EPQ recheck plan tree, which is problematic at least in the
foreign-join case, because such a foreign join isn't guaranteed to have
an alternative local-join plan required for running the recheck method
function; in the postgres_fdw case this could lead to a segmentation
fault or an assert failure in an assert-enabled build when running the
recheck method function.

Even if inside an EPQ recheck, any scan nodes that aren't descendant
ones in the EPQ recheck plan tree should be normally processed by using
the access method function; fix by modifying ExecScanFetch so that if
inside an EPQ recheck, it runs the recheck method function for
foreign/custom joins that are descendant nodes in the EPQ recheck plan
tree as before and runs the access method function for foreign/custom
joins that aren't.

This fix also adds to postgres_fdw an isolation test for an EPQ recheck
that caused issues stated above.

Oversight in commit 385f337c9.

Reported-by: Kristian Lejao <[email protected]>
Author: Masahiko Sawada <[email protected]>
Co-authored-by: Etsuro Fujita <[email protected]>
Reviewed-by: Michael Paquier <[email protected]>
Reviewed-by: Etsuro Fujita <[email protected]>
Discussion: https://postgr.es/m/CAD21AoBpo6Gx55FBOW+9s5X=nUw3Xpq64v35fpDEKsTERnc4TQ@mail.gmail.com
Backpatch-through: 13

contrib/postgres_fdw/.gitignore
contrib/postgres_fdw/Makefile
contrib/postgres_fdw/expected/eval_plan_qual.out [new file with mode: 0644]
contrib/postgres_fdw/meson.build
contrib/postgres_fdw/specs/eval_plan_qual.spec [new file with mode: 0644]
src/include/executor/execScan.h

index 5dcb3ff9723501c3fe639bee1c1435e47a580a6f..b4903eba657fa1a1cf0e406ff35ce5b9c8ba2972 100644 (file)
@@ -1,4 +1,6 @@
 # Generated subdirectories
 /log/
 /results/
+/output_iso/
 /tmp_check/
+/tmp_check_iso/
index adfbd2ef758e014a316c3f41119630820b72bed1..8eaf4d263b688db911fbf91d6c2e1e583ec11c9c 100644 (file)
@@ -17,6 +17,8 @@ EXTENSION = postgres_fdw
 DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql
 
 REGRESS = postgres_fdw query_cancel
+ISOLATION = eval_plan_qual
+ISOLATION_OPTS = --load-extension=postgres_fdw
 TAP_TESTS = 1
 
 ifdef USE_PGXS
diff --git a/contrib/postgres_fdw/expected/eval_plan_qual.out b/contrib/postgres_fdw/expected/eval_plan_qual.out
new file mode 100644 (file)
index 0000000..f3e3a22
--- /dev/null
@@ -0,0 +1,37 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s0_begin s0_update s1_begin s1_tuplock s0_commit s1_commit
+step s0_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s0_update: UPDATE a SET i = i + 1;
+step s1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1_tuplock: 
+    -- Verify if the sub-select has a foreign-join plan
+    EXPLAIN (VERBOSE, COSTS OFF)
+    SELECT a.i,
+        (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i)
+    FROM a FOR UPDATE;
+    SELECT a.i,
+        (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i)
+    FROM a FOR UPDATE;
+ <waiting ...>
+step s0_commit: COMMIT;
+step s1_tuplock: <... completed>
+QUERY PLAN                                                                                                                              
+----------------------------------------------------------------------------------------------------------------------------------------
+LockRows                                                                                                                                
+  Output: a.i, ((SubPlan expr_1)), a.ctid                                                                                               
+  ->  Seq Scan on public.a                                                                                                              
+        Output: a.i, (SubPlan expr_1), a.ctid                                                                                           
+        SubPlan expr_1                                                                                                                  
+          ->  Foreign Scan                                                                                                              
+                Output: 1                                                                                                               
+                Relations: (public.fb) INNER JOIN (public.fc)                                                                           
+                Remote SQL: SELECT NULL FROM (public.b r1 INNER JOIN public.c r2 ON (((r2.i = $1::integer)) AND ((r1.i = $1::integer))))
+(9 rows)
+
+i|?column?
+-+--------
+2|        
+(1 row)
+
+step s1_commit: COMMIT;
index 5c11bc6496fa89903b2b7e5c5f10316b2d77d5cf..aac89ffdde886259f711596c3c55813cf48777f5 100644 (file)
@@ -41,6 +41,12 @@ tests += {
     ],
     'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'],
   },
+  'isolation': {
+    'specs': [
+      'eval_plan_qual',
+    ],
+    'regress_args': ['--load-extension=postgres_fdw'],
+  },
   'tap': {
     'tests': [
       't/001_auth_scram.pl',
diff --git a/contrib/postgres_fdw/specs/eval_plan_qual.spec b/contrib/postgres_fdw/specs/eval_plan_qual.spec
new file mode 100644 (file)
index 0000000..30a83e0
--- /dev/null
@@ -0,0 +1,55 @@
+# Tests for the EvalPlanQual mechanism involving foreign tables
+
+setup
+{
+    DO $d$
+        BEGIN
+            EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
+                OPTIONS (dbname '$$||current_database()||$$',
+                         port '$$||current_setting('port')||$$'
+                )$$;
+        END;
+    $d$;
+    CREATE USER MAPPING FOR PUBLIC SERVER loopback;
+
+    CREATE TABLE a (i int);
+    CREATE TABLE b (i int);
+    CREATE TABLE c (i int);
+    CREATE FOREIGN TABLE fb (i int) SERVER loopback OPTIONS (table_name 'b');
+    CREATE FOREIGN TABLE fc (i int) SERVER loopback OPTIONS (table_name 'c');
+
+    INSERT INTO a VALUES (1);
+    INSERT INTO b VALUES (1);
+    INSERT INTO c VALUES (1);
+}
+
+teardown
+{
+    DROP TABLE a;
+    DROP TABLE b;
+    DROP TABLE c;
+    DROP SERVER loopback CASCADE;
+}
+
+session s0
+step s0_begin { BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s0_update { UPDATE a SET i = i + 1; }
+step s0_commit { COMMIT; }
+
+session s1
+step s1_begin { BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s1_tuplock {
+    -- Verify if the sub-select has a foreign-join plan
+    EXPLAIN (VERBOSE, COSTS OFF)
+    SELECT a.i,
+        (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i)
+    FROM a FOR UPDATE;
+    SELECT a.i,
+        (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i)
+    FROM a FOR UPDATE;
+}
+step s1_commit { COMMIT; }
+
+# This test exercises EvalPlanQual with a SubLink sub-select (which should
+# be unaffected by any EPQ recheck behavior in the outer query).
+permutation s0_begin s0_update s1_begin s1_tuplock s0_commit s1_commit
index 837ea7785bb4c814c72dc1d0e42b3b015fc2d745..2003cbc7ed562cb0f9fefe9376d8a9e83c3a4502 100644 (file)
@@ -49,16 +49,24 @@ ExecScanFetch(ScanState *node,
        {
            /*
             * This is a ForeignScan or CustomScan which has pushed down a
-            * join to the remote side.  The recheck method is responsible not
-            * only for rechecking the scan/join quals but also for storing
-            * the correct tuple in the slot.
+            * join to the remote side.  If it is a descendant node in the EPQ
+            * recheck plan tree, run the recheck method function.  Otherwise,
+            * run the access method function below.
             */
+           if (bms_is_member(epqstate->epqParam, node->ps.plan->extParam))
+           {
+               /*
+                * The recheck method is responsible not only for rechecking
+                * the scan/join quals but also for storing the correct tuple
+                * in the slot.
+                */
 
-           TupleTableSlot *slot = node->ss_ScanTupleSlot;
+               TupleTableSlot *slot = node->ss_ScanTupleSlot;
 
-           if (!(*recheckMtd) (node, slot))
-               ExecClearTuple(slot);   /* would not be returned by scan */
-           return slot;
+               if (!(*recheckMtd) (node, slot))
+                   ExecClearTuple(slot);   /* would not be returned by scan */
+               return slot;
+           }
        }
        else if (epqstate->relsubs_done[scanrelid - 1])
        {