From a54226f222fd52c1587594c73b3ebb9499c77dc5 Mon Sep 17 00:00:00 2001
From: Rafia Sabih <rafia.sabih@cybertec.at>
Date: Wed, 15 Jul 2026 16:51:16 +0530
Subject: [PATCH v13 2/2] postgres_fdw:Add streaming_fetch option for
 cursor-free fetching

postgres_fdw uses cursors to fetch tuples from the remote server
incrementally. While this works correctly, cursors prevent the remote
side from using parallel query execution, which can significantly limit
performance for large scans.

This adds a new boolean option streaming_fetch, available at both the
server and table level, that gives an alternate fetching mechanism.
Because no cursor is created on the remote side, the remote query is free
to use parallel execution.

When a second scan begins on the same connection while another is still
in progress, the remaining tuples of the active scan are drained into a
tuplestore and replayed when execution returns to that scan. This
preserves correct results across nested or interleaved scans without
requiring an additional connection.

streaming_fetch defaults to false, preserving existing behavior.
Asynchronous execution is not supported in this mode and is disabled
automatically when streaming_fetch is enabled.

Original idea: Bernd Helmle
Key suggestions: Robert Haas
---
 contrib/postgres_fdw/connection.c             |  21 +-
 .../postgres_fdw/expected/postgres_fdw.out    | 986 +++++++++++++++++-
 contrib/postgres_fdw/option.c                 |   4 +
 contrib/postgres_fdw/postgres_fdw.c           | 463 +++++++-
 contrib/postgres_fdw/postgres_fdw.h           |   7 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 410 ++++++++
 doc/src/sgml/postgres-fdw.sgml                |  15 +
 7 files changed, 1857 insertions(+), 49 deletions(-)

diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index aab21695979..2b298bd2a91 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -163,7 +163,6 @@ static void pgfdw_inval_callback(Datum arg, SysCacheIdentifier cacheid,
 								 uint32 hashvalue);
 static void pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry);
 static void pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel);
-static bool pgfdw_cancel_query(PGconn *conn);
 static bool pgfdw_cancel_query_begin(PGconn *conn, TimestampTz endtime);
 static bool pgfdw_cancel_query_end(PGconn *conn, TimestampTz endtime,
 								   TimestampTz retrycanceltime,
@@ -1090,6 +1089,16 @@ pgfdw_get_result(PGconn *conn)
 	return libpqsrv_get_result_last(conn, pgfdw_we_get_result);
 }
 
+/*
+ * Used in case of streaming_fetch mode.
+ * Caller is responsible for the error handling on the result.
+ */
+PGresult *
+pgfdw_get_next_result(PGconn *conn)
+{
+	return libpqsrv_get_result(conn, pgfdw_we_get_result);
+}
+
 /*
  * Report an error we got from the remote server.
  *
@@ -1567,7 +1576,7 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel)
  * query text from the pendingAreq saved in the per-connection state, then
  * report the query using it.
  */
-static bool
+bool
 pgfdw_cancel_query(PGconn *conn)
 {
 	TimestampTz now = GetCurrentTimestamp();
@@ -1927,8 +1936,10 @@ pgfdw_abort_cleanup(ConnCacheEntry *entry, bool toplevel)
 	 * an asynchronous fetch begun by fetch_more_data_begin() was not done
 	 * successfully and thus the per-connection state was not reset in
 	 * fetch_more_data(); in that case reset the per-connection state here.
+	 * Similarly for active_scan, if it is not consumed completely then reset
+	 * the state here.
 	 */
-	if (entry->state.pendingAreq)
+	if (entry->state.pendingAreq || entry->state.active_scan)
 		memset(&entry->state, 0, sizeof(entry->state));
 
 	/* Disarm changing_xact_state if it all worked */
@@ -2219,7 +2230,7 @@ pgfdw_finish_abort_cleanup(List *pending_entries, List *cancel_requested,
 		}
 
 		/* Reset the per-connection state if needed */
-		if (entry->state.pendingAreq)
+		if (entry->state.pendingAreq || entry->state.active_scan)
 			memset(&entry->state, 0, sizeof(entry->state));
 
 		/* We're done with this entry; unset the changing_xact_state flag */
@@ -2264,7 +2275,7 @@ pgfdw_finish_abort_cleanup(List *pending_entries, List *cancel_requested,
 		entry->have_error = false;
 
 		/* Reset the per-connection state if needed */
-		if (entry->state.pendingAreq)
+		if (entry->state.pendingAreq || entry->state.active_scan)
 			memset(&entry->state, 0, sizeof(entry->state));
 
 		/* We're done with this entry; unset the changing_xact_state flag */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 048f624ba0c..101eed70ffb 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -277,6 +277,363 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1;  -- should work again
 ANALYZE ft1;
 ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true');
 -- ===================================================================
+-- test streaming_fetch option
+-- ===================================================================
+CREATE SERVER fetch_stream_srv
+    FOREIGN DATA WRAPPER postgres_fdw
+    OPTIONS (dbname :'current_database', port :'current_port');
+CREATE USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+CREATE TABLE local_tbl (id int, val text);
+INSERT INTO local_tbl VALUES (1, 'a'), (2, 'b'), (3, 'c');
+-- 1. streaming_fetch set at SERVER level only
+-- 1a. Set streaming_fetch = true on the server
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+CREATE FOREIGN TABLE ft_server (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+-- Verify option appears in foreign server
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+     srvname      |   option_name   | option_value 
+------------------+-----------------+--------------
+ fetch_stream_srv | streaming_fetch | true
+(1 row)
+
+-- Verify option is NOT present at table level (inherits from server)
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                 ftoptions                 
+-----------+-------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+-- 1b. Switch server-level to streaming_fetch = false
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+     srvname      |   option_name   | option_value 
+------------------+-----------------+--------------
+ fetch_stream_srv | streaming_fetch | false
+(1 row)
+
+-- Verify option is NOT present at table level (inherits from server)
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                 ftoptions                 
+-----------+-------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+-- 2. streaming_fetch set at TABLE level only (no server-level option)
+ALTER SERVER fetch_stream_srv OPTIONS (DROP streaming_fetch);
+-- 2a. streaming_fetch = true at table level
+ALTER FOREIGN TABLE ft_server OPTIONS (ADD streaming_fetch 'true');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                           ftoptions                            
+-----------+----------------------------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl,streaming_fetch=true}
+(1 row)
+
+SELECT * FROM ft_server ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- 2b. streaming_fetch = false at table level
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                            ftoptions                            
+-----------+-----------------------------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl,streaming_fetch=false}
+(1 row)
+
+SELECT * FROM ft_server ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- 3. TABLE-level value overrides SERVER-level value
+-- 3a. Server = true, Table = false
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+-- Server  shows true
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+     srvname      |   option_name   | option_value 
+------------------+-----------------+--------------
+ fetch_stream_srv | streaming_fetch | true
+(1 row)
+
+-- Table shows its own override: false
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                            ftoptions                            
+-----------+-----------------------------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl,streaming_fetch=false}
+(1 row)
+
+-- Query must succeed and use table-level setting (false)
+SELECT * FROM ft_server ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- 3b. Server = false, Table = true  →  effective value is true
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'true');
+-- Server shows false
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+     srvname      |   option_name   | option_value 
+------------------+-----------------+--------------
+ fetch_stream_srv | streaming_fetch | false
+(1 row)
+
+-- Table shows its own override: true
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+  ftrelid  |                           ftoptions                            
+-----------+----------------------------------------------------------------
+ ft_server | {schema_name=public,table_name=local_tbl,streaming_fetch=true}
+(1 row)
+
+-- Query must succeed and use table-level setting (true)
+SELECT * FROM ft_server ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- 4. Negative tests: invalid values must be rejected
+-- streaming_fetch is boolean; non-boolean value must ERROR
+\set VERBOSITY terse
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch 'yes');  -- ERROR
+ERROR:  streaming_fetch requires a Boolean value
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch '1');    -- ERROR
+ERROR:  streaming_fetch requires a Boolean value
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch '');     -- ERROR
+ERROR:  streaming_fetch requires a Boolean value
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'notabool');             -- ERROR
+ERROR:  streaming_fetch requires a Boolean value
+\set VERBOSITY default
+-- 5. ALTER FOREIGN TABLE: add, change, and drop streaming_fetch
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'true');
+CREATE FOREIGN TABLE ft_alter_test (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+-- No table-level option yet
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+    ftrelid    |                 ftoptions                 
+---------------+-------------------------------------------
+ ft_alter_test | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+-- ADD table-level option
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (ADD streaming_fetch 'false');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+    ftrelid    |                            ftoptions                            
+---------------+-----------------------------------------------------------------
+ ft_alter_test | {schema_name=public,table_name=local_tbl,streaming_fetch=false}
+(1 row)
+
+SELECT * FROM ft_alter_test ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- SET (change) table-level option
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (SET streaming_fetch 'true');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+    ftrelid    |                           ftoptions                            
+---------------+----------------------------------------------------------------
+ ft_alter_test | {schema_name=public,table_name=local_tbl,streaming_fetch=true}
+(1 row)
+
+SELECT * FROM ft_alter_test ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+-- DROP table-level option (falls back to server-level)
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (DROP streaming_fetch);
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+    ftrelid    |                 ftoptions                 
+---------------+-------------------------------------------
+ ft_alter_test | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+SELECT * FROM ft_alter_test ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+DROP FOREIGN TABLE ft_alter_test;
+-- 6. streaming_fetch with non-default fetch_size values
+-- Use a 12-row table so chunk boundaries are distinct and predictable:
+-- fetch_size=1 gives 12 single-row chunks, fetch_size=5 gives chunks
+-- of 5+5+2, and fetch_size=1000 puts all rows in a single chunk.
+CREATE TABLE local_tbl_large (id int, val text);
+INSERT INTO local_tbl_large SELECT id, 'val' || id FROM generate_series(1, 12) id;
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'true');
+-- fetch_size = 1: every row is its own libpq chunk; exercises the path
+-- where pgfdw_get_next_result is called once per row.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '1');
+SELECT count(*) FROM ft_fetchsize;
+ count 
+-------
+    12
+(1 row)
+
+DROP FOREIGN TABLE ft_fetchsize;
+-- fetch_size = 5: three chunks with a partial last chunk (5+5+2);
+-- the final chunk is smaller than fetch_size.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '5');
+SELECT count(*) FROM ft_fetchsize;
+ count 
+-------
+    12
+(1 row)
+
+SELECT * FROM ft_fetchsize ORDER BY id;
+ id |  val  
+----+-------
+  1 | val1
+  2 | val2
+  3 | val3
+  4 | val4
+  5 | val5
+  6 | val6
+  7 | val7
+  8 | val8
+  9 | val9
+ 10 | val10
+ 11 | val11
+ 12 | val12
+(12 rows)
+
+DROP FOREIGN TABLE ft_fetchsize;
+-- fetch_size exceeds the table row count: all rows arrive in one chunk
+-- followed immediately by the final empty PGRES_TUPLES_OK result.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '1000');
+SELECT count(*) FROM ft_fetchsize;
+ count 
+-------
+    12
+(1 row)
+
+DROP FOREIGN TABLE ft_fetchsize;
+DROP TABLE local_tbl_large;
+-- 7. streaming_fetch combined with use_remote_estimate
+-- use_remote_estimate issues a remote EXPLAIN to size the scan at plan
+-- time; streaming_fetch must not interfere with that EXPLAIN call.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD use_remote_estimate 'true');
+CREATE FOREIGN TABLE ft_remote_est (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+-- Verify both options are active at the server level.
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch', 'use_remote_estimate')
+ORDER BY option_name;
+     srvname      |     option_name     | option_value 
+------------------+---------------------+--------------
+ fetch_stream_srv | streaming_fetch     | true
+ fetch_stream_srv | use_remote_estimate | true
+(2 rows)
+
+-- Both options active: use_remote_estimate sizes the scan remotely via
+-- EXPLAIN, then streaming_fetch fetches rows without a cursor.
+SELECT * FROM ft_remote_est ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+ALTER SERVER fetch_stream_srv OPTIONS (DROP use_remote_estimate);
+-- 9. streaming_fetch check the output in explain with verbose
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft_remote_est ORDER BY id;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Foreign Scan on public.ft_remote_est
+   Output: id, val
+   Remote SQL: SELECT id, val FROM public.local_tbl ORDER BY id ASC NULLS LAST
+   streaming_fetch: true
+(4 rows)
+
+-- Cleanup
+DROP FOREIGN TABLE ft_remote_est;
+DROP USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+DROP SERVER fetch_stream_srv CASCADE;
+NOTICE:  drop cascades to foreign table ft_server
+DROP TABLE local_tbl;
+-- ===================================================================
 -- test subscription
 -- ===================================================================
 CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1
@@ -448,6 +805,89 @@ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
  1000 |  0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0  | 0          | foo
 (1 row)
 
+-- Test in streaming_fetch mode to cover path from subquery params
+-- with only one table using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (streaming_fetch 'true');
+SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+  c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
+------+----+-------+------------------------------+--------------------------+----+------------+-----
+ 1000 |  0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0  | 0          | foo
+(1 row)
+
+-- Test join with only one table using streaming_fetch at a time
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+ c1  | C 1 
+-----+-----
+ 101 | 101
+ 102 | 102
+ 103 | 103
+ 104 | 104
+ 105 | 105
+ 106 | 106
+ 107 | 107
+ 108 | 108
+ 109 | 109
+ 110 | 110
+(10 rows)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (streaming_fetch 'true');
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+ c1  | C 1 
+-----+-----
+ 101 | 101
+ 102 | 102
+ 103 | 103
+ 104 | 104
+ 105 | 105
+ 106 | 106
+ 107 | 107
+ 108 | 108
+ 109 | 109
+ 110 | 110
+(10 rows)
+
+-- with both the tables using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+  c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
+------+----+-------+------------------------------+--------------------------+----+------------+-----
+ 1000 |  0 | 01000 | Thu Jan 01 00:00:00 1970 PST | Thu Jan 01 00:00:00 1970 | 0  | 0          | foo
+(1 row)
+
+-- Test join with both the tables using streaming_fetch
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+ c1  | C 1 
+-----+-----
+ 101 | 101
+ 102 | 102
+ 103 | 103
+ 104 | 104
+ 105 | 105
+ 106 | 106
+ 107 | 107
+ 108 | 108
+ 109 | 109
+ 110 | 110
+(10 rows)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- streaming_fetch: verify correct results when parallel-friendly settings
+-- are active locally.  With no cursor on the remote side, the remote
+-- planner is free to choose a parallel plan; results must match exactly.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SET max_parallel_workers_per_gather = 2;
+SET min_parallel_table_scan_size = 0;
+SELECT count(*) FROM ft1;
+ count 
+-------
+  1000
+(1 row)
+
+RESET max_parallel_workers_per_gather;
+RESET min_parallel_table_scan_size;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
 -- used in CTE
 WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
  c1 | c2 |  c3   |              c4              
@@ -784,16 +1224,265 @@ EXPLAIN (VERBOSE, COSTS OFF)
          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = $1::integer))
 (8 rows)
 
-SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2;
- C 1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
------+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+-------+------------------------------+--------------------------+----+------------+-----
-  47 |  7 | 00047 | Tue Feb 17 00:00:00 1970 PST | Tue Feb 17 00:00:00 1970 | 7  | 7          | foo |  7 |  7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7  | 7          | foo
+SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2;
+ C 1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+-------+------------------------------+--------------------------+----+------------+-----
+  47 |  7 | 00047 | Tue Feb 17 00:00:00 1970 PST | Tue Feb 17 00:00:00 1970 | 7  | 7          | foo |  7 |  7 | 00007 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7  | 7          | foo
+(1 row)
+
+-- check both safe and unsafe join conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+  SELECT * FROM ft2 a, ft2 b
+  WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+                                                 QUERY PLAN                                                  
+-------------------------------------------------------------------------------------------------------------
+ Nested Loop
+   Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8, b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
+   ->  Foreign Scan on public.ft2 a
+         Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8
+         Filter: (a.c8 = 'foo'::user_enum)
+         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c2 = 6))
+   ->  Foreign Scan on public.ft2 b
+         Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
+         Filter: ((b.c7)::text = upper((a.c7)::text))
+         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
+(10 rows)
+
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+   6 |  6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo |   6 |  6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+  16 |  6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo |  16 |  6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+  26 |  6 | 00026 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo |  26 |  6 | 00026 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+  36 |  6 | 00036 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo |  36 |  6 | 00036 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+  46 |  6 | 00046 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo |  46 |  6 | 00046 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+  56 |  6 | 00056 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo |  56 |  6 | 00056 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+  66 |  6 | 00066 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo |  66 |  6 | 00066 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+  76 |  6 | 00076 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo |  76 |  6 | 00076 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+  86 |  6 | 00086 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo |  86 |  6 | 00086 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+  96 |  6 | 00096 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo |  96 |  6 | 00096 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 106 |  6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 106 |  6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 116 |  6 | 00116 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 116 |  6 | 00116 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 126 |  6 | 00126 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 126 |  6 | 00126 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 136 |  6 | 00136 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 136 |  6 | 00136 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 146 |  6 | 00146 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 146 |  6 | 00146 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 156 |  6 | 00156 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 156 |  6 | 00156 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 166 |  6 | 00166 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 166 |  6 | 00166 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 176 |  6 | 00176 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 176 |  6 | 00176 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 186 |  6 | 00186 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 186 |  6 | 00186 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 196 |  6 | 00196 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 196 |  6 | 00196 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 206 |  6 | 00206 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 206 |  6 | 00206 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 216 |  6 | 00216 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 216 |  6 | 00216 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 226 |  6 | 00226 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 226 |  6 | 00226 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 236 |  6 | 00236 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 236 |  6 | 00236 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 246 |  6 | 00246 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 246 |  6 | 00246 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 256 |  6 | 00256 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 256 |  6 | 00256 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 266 |  6 | 00266 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 266 |  6 | 00266 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 276 |  6 | 00276 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 276 |  6 | 00276 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 286 |  6 | 00286 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 286 |  6 | 00286 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 296 |  6 | 00296 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 296 |  6 | 00296 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 306 |  6 | 00306 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 306 |  6 | 00306 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 316 |  6 | 00316 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 316 |  6 | 00316 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 326 |  6 | 00326 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 326 |  6 | 00326 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 336 |  6 | 00336 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 336 |  6 | 00336 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 346 |  6 | 00346 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 346 |  6 | 00346 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 356 |  6 | 00356 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 356 |  6 | 00356 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 366 |  6 | 00366 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 366 |  6 | 00366 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 376 |  6 | 00376 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 376 |  6 | 00376 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 386 |  6 | 00386 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 386 |  6 | 00386 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 396 |  6 | 00396 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 396 |  6 | 00396 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 406 |  6 | 00406 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 406 |  6 | 00406 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 416 |  6 | 00416 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 416 |  6 | 00416 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 426 |  6 | 00426 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 426 |  6 | 00426 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 436 |  6 | 00436 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 436 |  6 | 00436 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 446 |  6 | 00446 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 446 |  6 | 00446 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 456 |  6 | 00456 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 456 |  6 | 00456 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 466 |  6 | 00466 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 466 |  6 | 00466 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 476 |  6 | 00476 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 476 |  6 | 00476 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 486 |  6 | 00486 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 486 |  6 | 00486 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 496 |  6 | 00496 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 496 |  6 | 00496 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 506 |  6 | 00506 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 506 |  6 | 00506 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 516 |  6 | 00516 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 516 |  6 | 00516 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 526 |  6 | 00526 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 526 |  6 | 00526 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 536 |  6 | 00536 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 536 |  6 | 00536 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 546 |  6 | 00546 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 546 |  6 | 00546 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 556 |  6 | 00556 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 556 |  6 | 00556 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 566 |  6 | 00566 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 566 |  6 | 00566 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 576 |  6 | 00576 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 576 |  6 | 00576 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 586 |  6 | 00586 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 586 |  6 | 00586 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 596 |  6 | 00596 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 596 |  6 | 00596 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 606 |  6 | 00606 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 606 |  6 | 00606 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 616 |  6 | 00616 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 616 |  6 | 00616 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 626 |  6 | 00626 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 626 |  6 | 00626 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 636 |  6 | 00636 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 636 |  6 | 00636 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 646 |  6 | 00646 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 646 |  6 | 00646 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 656 |  6 | 00656 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 656 |  6 | 00656 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 666 |  6 | 00666 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 666 |  6 | 00666 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 676 |  6 | 00676 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 676 |  6 | 00676 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 686 |  6 | 00686 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 686 |  6 | 00686 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 696 |  6 | 00696 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 696 |  6 | 00696 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 706 |  6 | 00706 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 706 |  6 | 00706 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 716 |  6 | 00716 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 716 |  6 | 00716 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 726 |  6 | 00726 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 726 |  6 | 00726 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 736 |  6 | 00736 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 736 |  6 | 00736 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 746 |  6 | 00746 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 746 |  6 | 00746 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 756 |  6 | 00756 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 756 |  6 | 00756 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 766 |  6 | 00766 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 766 |  6 | 00766 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 776 |  6 | 00776 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 776 |  6 | 00776 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 786 |  6 | 00786 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 786 |  6 | 00786 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 796 |  6 | 00796 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 796 |  6 | 00796 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 806 |  6 | 00806 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 806 |  6 | 00806 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 816 |  6 | 00816 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 816 |  6 | 00816 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 826 |  6 | 00826 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 826 |  6 | 00826 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 836 |  6 | 00836 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 836 |  6 | 00836 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 846 |  6 | 00846 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 846 |  6 | 00846 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 856 |  6 | 00856 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 856 |  6 | 00856 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 866 |  6 | 00866 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 866 |  6 | 00866 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 876 |  6 | 00876 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 876 |  6 | 00876 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 886 |  6 | 00886 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 886 |  6 | 00886 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 896 |  6 | 00896 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 896 |  6 | 00896 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 906 |  6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 906 |  6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 916 |  6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 916 |  6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 926 |  6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 926 |  6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 936 |  6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 936 |  6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 946 |  6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 946 |  6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 956 |  6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 956 |  6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 966 |  6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 966 |  6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 976 |  6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 976 |  6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 986 |  6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 986 |  6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+(100 rows)
+
+-- Test in streaming_fetch mode for rescan path
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+   6 |  6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo |   6 |  6 | 00006 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+  16 |  6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo |  16 |  6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+  26 |  6 | 00026 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo |  26 |  6 | 00026 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+  36 |  6 | 00036 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo |  36 |  6 | 00036 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+  46 |  6 | 00046 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo |  46 |  6 | 00046 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+  56 |  6 | 00056 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo |  56 |  6 | 00056 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+  66 |  6 | 00066 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo |  66 |  6 | 00066 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+  76 |  6 | 00076 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo |  76 |  6 | 00076 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+  86 |  6 | 00086 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo |  86 |  6 | 00086 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+  96 |  6 | 00096 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo |  96 |  6 | 00096 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 106 |  6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 106 |  6 | 00106 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 116 |  6 | 00116 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 116 |  6 | 00116 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 126 |  6 | 00126 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 126 |  6 | 00126 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 136 |  6 | 00136 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 136 |  6 | 00136 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 146 |  6 | 00146 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 146 |  6 | 00146 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 156 |  6 | 00156 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 156 |  6 | 00156 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 166 |  6 | 00166 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 166 |  6 | 00166 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 176 |  6 | 00176 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 176 |  6 | 00176 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 186 |  6 | 00186 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 186 |  6 | 00186 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 196 |  6 | 00196 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 196 |  6 | 00196 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 206 |  6 | 00206 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 206 |  6 | 00206 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 216 |  6 | 00216 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 216 |  6 | 00216 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 226 |  6 | 00226 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 226 |  6 | 00226 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 236 |  6 | 00236 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 236 |  6 | 00236 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 246 |  6 | 00246 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 246 |  6 | 00246 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 256 |  6 | 00256 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 256 |  6 | 00256 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 266 |  6 | 00266 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 266 |  6 | 00266 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 276 |  6 | 00276 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 276 |  6 | 00276 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 286 |  6 | 00286 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 286 |  6 | 00286 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 296 |  6 | 00296 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 296 |  6 | 00296 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 306 |  6 | 00306 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 306 |  6 | 00306 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 316 |  6 | 00316 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 316 |  6 | 00316 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 326 |  6 | 00326 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 326 |  6 | 00326 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 336 |  6 | 00336 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 336 |  6 | 00336 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 346 |  6 | 00346 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 346 |  6 | 00346 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 356 |  6 | 00356 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 356 |  6 | 00356 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 366 |  6 | 00366 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 366 |  6 | 00366 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 376 |  6 | 00376 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 376 |  6 | 00376 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 386 |  6 | 00386 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 386 |  6 | 00386 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 396 |  6 | 00396 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 396 |  6 | 00396 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 406 |  6 | 00406 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 406 |  6 | 00406 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 416 |  6 | 00416 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 416 |  6 | 00416 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 426 |  6 | 00426 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 426 |  6 | 00426 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 436 |  6 | 00436 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 436 |  6 | 00436 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 446 |  6 | 00446 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 446 |  6 | 00446 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 456 |  6 | 00456 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 456 |  6 | 00456 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 466 |  6 | 00466 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 466 |  6 | 00466 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 476 |  6 | 00476 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 476 |  6 | 00476 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 486 |  6 | 00486 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 486 |  6 | 00486 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 496 |  6 | 00496 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 496 |  6 | 00496 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 506 |  6 | 00506 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 506 |  6 | 00506 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 516 |  6 | 00516 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 516 |  6 | 00516 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 526 |  6 | 00526 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 526 |  6 | 00526 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 536 |  6 | 00536 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 536 |  6 | 00536 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 546 |  6 | 00546 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 546 |  6 | 00546 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 556 |  6 | 00556 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 556 |  6 | 00556 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 566 |  6 | 00566 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 566 |  6 | 00566 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 576 |  6 | 00576 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 576 |  6 | 00576 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 586 |  6 | 00586 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 586 |  6 | 00586 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 596 |  6 | 00596 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 596 |  6 | 00596 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 606 |  6 | 00606 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 606 |  6 | 00606 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 616 |  6 | 00616 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 616 |  6 | 00616 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 626 |  6 | 00626 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 626 |  6 | 00626 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 636 |  6 | 00636 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 636 |  6 | 00636 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 646 |  6 | 00646 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 646 |  6 | 00646 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 656 |  6 | 00656 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 656 |  6 | 00656 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 666 |  6 | 00666 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 666 |  6 | 00666 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 676 |  6 | 00676 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 676 |  6 | 00676 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 686 |  6 | 00686 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 686 |  6 | 00686 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 696 |  6 | 00696 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 696 |  6 | 00696 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 706 |  6 | 00706 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 706 |  6 | 00706 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 716 |  6 | 00716 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 716 |  6 | 00716 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 726 |  6 | 00726 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 726 |  6 | 00726 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 736 |  6 | 00736 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 736 |  6 | 00736 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 746 |  6 | 00746 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 746 |  6 | 00746 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 756 |  6 | 00756 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 756 |  6 | 00756 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 766 |  6 | 00766 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 766 |  6 | 00766 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 776 |  6 | 00776 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 776 |  6 | 00776 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 786 |  6 | 00786 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 786 |  6 | 00786 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 796 |  6 | 00796 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 796 |  6 | 00796 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 806 |  6 | 00806 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 806 |  6 | 00806 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 816 |  6 | 00816 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 816 |  6 | 00816 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 826 |  6 | 00826 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 826 |  6 | 00826 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 836 |  6 | 00836 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 836 |  6 | 00836 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 846 |  6 | 00846 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 846 |  6 | 00846 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 856 |  6 | 00856 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 856 |  6 | 00856 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 866 |  6 | 00866 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 866 |  6 | 00866 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 876 |  6 | 00876 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 876 |  6 | 00876 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 886 |  6 | 00886 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 886 |  6 | 00886 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 896 |  6 | 00896 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 896 |  6 | 00896 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+ 906 |  6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo | 906 |  6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6  | 6          | foo
+ 916 |  6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo | 916 |  6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6  | 6          | foo
+ 926 |  6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo | 926 |  6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6  | 6          | foo
+ 936 |  6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo | 936 |  6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6  | 6          | foo
+ 946 |  6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo | 946 |  6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6  | 6          | foo
+ 956 |  6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo | 956 |  6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6  | 6          | foo
+ 966 |  6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo | 966 |  6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6  | 6          | foo
+ 976 |  6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo | 976 |  6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6  | 6          | foo
+ 986 |  6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo | 986 |  6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6  | 6          | foo
+ 996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
+(100 rows)
+
+-- Test for streaming_fetch covering rescans and three active cursors
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1 AND a.c8 = 'foo'
+AND b.c7 = upper(a.c7);
+ count 
+-------
+   100
 (1 row)
 
--- check both safe and unsafe join conditions
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- Test in streaming_fetch mode for interleaved scans.
+-- The non-shippable condition a.c8 = 'foo' (user_enum is not a built-in type)
+-- prevents full join pushdown, so the planner issues two separate FDW scans
+-- that share the same loopback connection.  When the inner scan's init_scan
+-- fires it calls save_to_tuplestore to drain the outer scan's unread rows.
+ ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+-- Show the plan: ft2 must appear as two independent ForeignScan nodes, not
+-- a single pushed-down remote join.
 EXPLAIN (VERBOSE, COSTS OFF)
-  SELECT * FROM ft2 a, ft2 b
-  WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
                                                  QUERY PLAN                                                  
 -------------------------------------------------------------------------------------------------------------
  Nested Loop
@@ -802,12 +1491,16 @@ EXPLAIN (VERBOSE, COSTS OFF)
          Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8
          Filter: (a.c8 = 'foo'::user_enum)
          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c2 = 6))
+         streaming_fetch: true
    ->  Foreign Scan on public.ft2 b
          Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
          Filter: ((b.c7)::text = upper((a.c7)::text))
          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
-(10 rows)
+         streaming_fetch: true
+(12 rows)
 
+-- Verify results with cursor path.
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
 SELECT * FROM ft2 a, ft2 b
 WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
  c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1  | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
@@ -914,6 +1607,54 @@ WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
  996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo | 996 |  6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6  | 6          | foo
 (100 rows)
 
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+-- Three-way self-join to test streaming_fetch
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
+ Aggregate
+   Output: count(*)
+   ->  Nested Loop
+         ->  Nested Loop
+               Output: a.c1, b.c1
+               ->  Foreign Scan on public.ft2 a
+                     Output: a.c1, a.c7
+                     Filter: (a.c8 = 'foo'::user_enum)
+                     Remote SQL: SELECT "C 1", c7, c8 FROM "S 1"."T 1" WHERE ((c2 = 6))
+                     streaming_fetch: true
+               ->  Foreign Scan on public.ft2 b
+                     Output: b.c1, b.c7
+                     Filter: ((b.c7)::text = upper((a.c7)::text))
+                     Remote SQL: SELECT "C 1", c7 FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
+                     streaming_fetch: true
+         ->  Foreign Scan on public.ft2 c
+               Output: c.c1
+               Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
+               streaming_fetch: true
+(19 rows)
+
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ count 
+-------
+   100
+(1 row)
+
+-- output matches in cursor mode
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ count 
+-------
+   100
+(1 row)
+
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
 -- bug before 9.3.5 due to sloppy handling of remote-estimate parameters
 SELECT * FROM ft1 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft2 WHERE c1 < 5));
  c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
@@ -2183,6 +2924,43 @@ SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2
  119
 (10 rows)
 
+-- Test in streaming_fetch mode to cover the patch for two simultaneous active cursors
+-- with only one table using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+ c1  
+-----
+ 110
+ 111
+ 112
+ 113
+ 114
+ 115
+ 116
+ 117
+ 118
+ 119
+(10 rows)
+
+-- with both the tables using streaming_fetch
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+ c1  
+-----
+ 110
+ 111
+ 112
+ 113
+ 114
+ 115
+ 116
+ 117
+ 118
+ 119
+(10 rows)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
 -- CROSS JOIN can be pushed down
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
@@ -2234,6 +3012,16 @@ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t
 ----+----
 (0 rows)
 
+-- Test in streaming_fetch mode to cover the case with multiple cursors but only one active cursor at a time
+ALTER FOREIGN TABLE ft5 OPTIONS (streaming_fetch 'true');
+ALTER FOREIGN TABLE ft6 OPTIONS (streaming_fetch 'true');
+SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ c1 | c1 
+----+----
+(0 rows)
+
+ALTER FOREIGN TABLE ft5 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft6 OPTIONS (SET streaming_fetch 'false');
 -- unsafe join conditions (c8 has a UDT), not pushed down. Practically a CROSS
 -- JOIN since c8 in both tables has same value.
 EXPLAIN (VERBOSE, COSTS OFF)
@@ -2970,6 +3758,72 @@ select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (ran
    100 | 49600 | 496.0000000000000000 |   1 | 991 |      0 | 49600
 (1 row)
 
+-- Test with limit and streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2 limit 1;
+ count |  sum  |         avg          | min | max | stddev | sum2  
+-------+-------+----------------------+-----+-----+--------+-------
+   100 | 49600 | 496.0000000000000000 |   1 | 991 |      0 | 49600
+(1 row)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+-- Test LIMIT stopping before all tuples are consumed.
+-- The WHERE clause references c8 (a user-defined type that cannot be
+-- pushed to the remote), preventing LIMIT pushdown.  The remote
+-- therefore streams all rows, and end_scan must discard the in-flight
+-- data when the local executor stops early.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+-- LIMIT 5 with default fetch_size=100: stops well within the first
+-- chunk; end_scan discards ~995 rows still in flight on the connection.
+SELECT c1 FROM ft1 WHERE c8 = 'foo' ORDER BY c1 LIMIT 5;
+ c1 
+----
+  1
+  2
+  3
+  4
+  5
+(5 rows)
+
+-- Verify the connection is still usable after the early stop.
+SELECT count(*) FROM ft1;
+ count 
+-------
+  1000
+(1 row)
+
+-- fetch_size=10, LIMIT=15: consumes one full chunk (rows 1-10) plus 5
+-- rows from a second chunk (rows 11-15); end_scan then discards the
+-- remainder of that chunk and all subsequent in-flight chunks.
+ALTER FOREIGN TABLE ft1 OPTIONS (fetch_size '10');
+SELECT c1 FROM ft1 WHERE c8 = 'foo' ORDER BY c1 LIMIT 15;
+ c1 
+----
+  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+(15 rows)
+
+SELECT count(*) FROM ft1;
+ count 
+-------
+  1000
+(1 row)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
 -- Aggregate is not pushed down as aggregation contains random()
 explain (verbose, costs off)
 select sum(c1 * (random() <= 1)::int) as sum, avg(c1) from ft1;
@@ -10940,6 +11794,48 @@ SELECT 1 FROM ft1 LIMIT 1;    -- should fail
 ERROR:  08006
 \set VERBOSITY default
 COMMIT;
+-- ===================================================================
+-- streaming_fetch: error recovery when the remote backend terminates
+-- ===================================================================
+-- Enable streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+-- Establish a fresh remote connection.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- Terminate the remote backend and wait for the termination to complete.
+DO $$ BEGIN
+PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity
+	WHERE application_name = 'fdw_retry_check';
+END $$;
+-- After the connection is broken, a streaming_fetch query should detect
+-- the broken connection, reestablish it, and succeed.
+BEGIN;
+SELECT c1 FROM ft1 ORDER BY c1 LIMIT 3;
+ c1 
+----
+  1
+  3
+  4
+(3 rows)
+
+-- Inside a subtransaction the broken connection must not be silently
+-- retried; the query should fail.
+DO $$ BEGIN
+PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity
+	WHERE application_name = 'fdw_retry_check';
+END $$;
+SAVEPOINT s2;
+-- The text of the error might vary across platforms, so only show SQLSTATE.
+\set VERBOSITY sqlstate
+SELECT 1 FROM ft1 LIMIT 1;    -- should fail
+ERROR:  08006
+\set VERBOSITY default
+COMMIT;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
 -- =============================================================================
 -- test connection invalidation cases and postgres_fdw_get_connections function
 -- =============================================================================
@@ -12103,6 +12999,80 @@ SELECT * FROM local_tbl, async_pt WHERE local_tbl.a = async_pt.a AND local_tbl.c
 
 ALTER FOREIGN TABLE async_p1 OPTIONS (DROP use_remote_estimate);
 ALTER FOREIGN TABLE async_p2 OPTIONS (DROP use_remote_estimate);
+-- Test with streaming_fetch
+-- No streaming_fetch at server, this should give Async Foreign Scan for for async_p1 and async_p2
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Append
+   ->  Async Foreign Scan on public.async_p1 async_pt_1
+         Output: async_pt_1.a, async_pt_1.b, async_pt_1.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl1
+   ->  Async Foreign Scan on public.async_p2 async_pt_2
+         Output: async_pt_2.a, async_pt_2.b, async_pt_2.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl2
+   ->  Seq Scan on public.async_p3 async_pt_3
+         Output: async_pt_3.a, async_pt_3.b, async_pt_3.c
+(9 rows)
+
+-- streaming_fetch = false at loopback server, this should still give Async Foreign Scan for async_p1 and async_p2
+ALTER SERVER loopback OPTIONS (streaming_fetch 'false');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Append
+   ->  Async Foreign Scan on public.async_p1 async_pt_1
+         Output: async_pt_1.a, async_pt_1.b, async_pt_1.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl1
+   ->  Async Foreign Scan on public.async_p2 async_pt_2
+         Output: async_pt_2.a, async_pt_2.b, async_pt_2.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl2
+   ->  Seq Scan on public.async_p3 async_pt_3
+         Output: async_pt_3.a, async_pt_3.b, async_pt_3.c
+(9 rows)
+
+-- streaming_fetch = false at loopback server but true for async_p1, this should give Foreign Scan for async_p1
+ALTER FOREIGN TABLE async_p1 OPTIONS (ADD streaming_fetch 'true');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Append
+   ->  Foreign Scan on public.async_p1 async_pt_1
+         Output: async_pt_1.a, async_pt_1.b, async_pt_1.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl1
+         streaming_fetch: true
+   ->  Async Foreign Scan on public.async_p2 async_pt_2
+         Output: async_pt_2.a, async_pt_2.b, async_pt_2.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl2
+   ->  Seq Scan on public.async_p3 async_pt_3
+         Output: async_pt_3.a, async_pt_3.b, async_pt_3.c
+(10 rows)
+
+-- streaming_fetch = true at loopback2 server, this should give Foreign Scan for async_p2 also
+ALTER SERVER loopback2 OPTIONS (streaming_fetch 'true');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Append
+   ->  Foreign Scan on public.async_p1 async_pt_1
+         Output: async_pt_1.a, async_pt_1.b, async_pt_1.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl1
+         streaming_fetch: true
+   ->  Foreign Scan on public.async_p2 async_pt_2
+         Output: async_pt_2.a, async_pt_2.b, async_pt_2.c
+         Remote SQL: SELECT a, b, c FROM public.base_tbl2
+         streaming_fetch: true
+   ->  Seq Scan on public.async_p3 async_pt_3
+         Output: async_pt_3.a, async_pt_3.b, async_pt_3.c
+(11 rows)
+
+ALTER FOREIGN TABLE async_p1 OPTIONS (DROP streaming_fetch);
+ALTER SERVER loopback OPTIONS (DROP streaming_fetch);
+ALTER SERVER loopback2 OPTIONS (DROP streaming_fetch);
 DROP TABLE local_tbl;
 DROP INDEX base_tbl1_idx;
 DROP INDEX base_tbl2_idx;
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 79b16c3f318..bd702523fe4 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -121,6 +121,7 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 			strcmp(def->defname, "parallel_commit") == 0 ||
 			strcmp(def->defname, "parallel_abort") == 0 ||
 			strcmp(def->defname, "keep_connections") == 0 ||
+			strcmp(def->defname, "streaming_fetch") == 0 ||
 			strcmp(def->defname, "restore_stats") == 0 ||
 			strcmp(def->defname, "use_scram_passthrough") == 0)
 		{
@@ -262,6 +263,9 @@ InitPgFdwOptions(void)
 		/* fetch_size is available on both server and table */
 		{"fetch_size", ForeignServerRelationId, false},
 		{"fetch_size", ForeignTableRelationId, false},
+		/* streaming_fetch is available on both server and table */
+		{"streaming_fetch", ForeignServerRelationId, false},
+		{"streaming_fetch", ForeignTableRelationId, false},
 		/* batch_size is available on both server and table */
 		{"batch_size", ForeignServerRelationId, false},
 		{"batch_size", ForeignTableRelationId, false},
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 3546c1217f9..5e3ef5b8483 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -54,6 +54,7 @@
 #include "utils/sampling.h"
 #include "utils/selfuncs.h"
 #include "utils/timestamp.h"
+#include "utils/tuplestore.h"
 
 PG_MODULE_MAGIC_EXT(
 					.name = "postgres_fdw",
@@ -84,6 +85,8 @@ enum FdwScanPrivateIndex
 	FdwScanPrivateRetrievedAttrs,
 	/* Integer representing the desired fetch_size */
 	FdwScanPrivateFetchSize,
+	/* Boolean indicating whether streaming_fetch mode is enabled */
+	FdwScanPrivateStreamingFetch,
 
 	/*
 	 * String describing join i.e. names of relations being joined and types
@@ -180,6 +183,12 @@ typedef struct PgFdwScanState
 	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
 
 	int			fetch_size;		/* number of tuples per fetch */
+	/* Required for the streaming_fetch mode */
+	bool		streaming_fetch;	/* set if the scan is using
+									 * streaming_fetch mode */
+	Tuplestorestate *tuplestore;	/* Tuplestore to save the tuples of the
+									 * query for later fetch. */
+	ForeignScanState *fsnode;	/* back-pointer, used by save_to_tuplestore */
 } PgFdwScanState;
 
 /*
@@ -639,6 +648,14 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
 							  const PgFdwRelationInfo *fpinfo_i);
 static int	get_batch_size_option(Relation rel);
 
+/* Only required for non-cursor mode */
+static void set_streaming_fetch(DefElem *def, PgFdwRelationInfo *fpinfo);
+static void drain_other_active_scan(PgFdwConnState *conn_state, PGconn *conn,
+									PgFdwScanState *self);
+static void fetch_from_tuplestore(ForeignScanState *node);
+static void save_to_tuplestore(PgFdwScanState *fsstate, PGconn *conn);
+static void init_scan(ForeignScanState *node);
+static bool is_active_scan(PgFdwScanState *fsstate);
 
 /*
  * Foreign-data wrapper handler function: return a struct with pointers
@@ -748,9 +765,12 @@ postgresGetForeignRelSize(PlannerInfo *root,
 	fpinfo->shippable_extensions = NIL;
 	fpinfo->fetch_size = 100;
 	fpinfo->async_capable = false;
+	fpinfo->streaming_fetch = false;
 
 	apply_server_options(fpinfo);
 	apply_table_options(fpinfo);
+	if (fpinfo->streaming_fetch)
+		fpinfo->async_capable = false;
 
 	/*
 	 * If the table or the server is configured to use remote estimates,
@@ -1510,9 +1530,9 @@ postgresGetForeignPlan(PlannerInfo *root,
 	 * Build the fdw_private list that will be available to the executor.
 	 * Items in the list must match order in enum FdwScanPrivateIndex.
 	 */
-	fdw_private = list_make3(makeString(sql.data),
+	fdw_private = list_make4(makeString(sql.data),
 							 retrieved_attrs,
-							 makeInteger(fpinfo->fetch_size));
+							 makeInteger(fpinfo->fetch_size), makeBoolean(fpinfo->streaming_fetch));
 	if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
 		fdw_private = lappend(fdw_private,
 							  makeString(fpinfo->relation_name));
@@ -1646,6 +1666,8 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
 												 FdwScanPrivateRetrievedAttrs);
 	fsstate->fetch_size = intVal(list_nth(fsplan->fdw_private,
 										  FdwScanPrivateFetchSize));
+	fsstate->streaming_fetch = boolVal(list_nth(fsplan->fdw_private,
+												FdwScanPrivateStreamingFetch));
 
 	/* Create contexts for batches of tuples and per-tuple temp workspace. */
 	fsstate->batch_cxt = AllocSetContextCreate(estate->es_query_cxt,
@@ -1687,6 +1709,8 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
 
 	/* Set the async-capable flag */
 	fsstate->async_capable = node->ss.ps.async_capable;
+	fsstate->tuplestore = NULL;
+	fsstate->fsnode = node;
 }
 
 /*
@@ -1707,7 +1731,12 @@ postgresIterateForeignScan(ForeignScanState *node)
 	 * first call after Begin or ReScan.
 	 */
 	if (!fsstate->scan_in_progress)
-		create_cursor(node);
+	{
+		if (fsstate->streaming_fetch)
+			init_scan(node);
+		else
+			create_cursor(node);
+	}
 
 	/*
 	 * Get some more tuples, if we've run out.
@@ -1744,7 +1773,8 @@ postgresReScanForeignScan(ForeignScanState *node)
 {
 	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
 	char		sql[64];
-	PGresult   *res;
+	PGresult   *res = NULL;
+	bool		reinitialize_scan = false;
 
 	/* If no scan is in progress, nothing to do. */
 	if (!fsstate->scan_in_progress)
@@ -1773,40 +1803,82 @@ postgresReScanForeignScan(ForeignScanState *node)
 	 */
 	if (node->ss.ps.chgParam != NULL)
 	{
-		fsstate->scan_in_progress = false;
-		snprintf(sql, sizeof(sql), "CLOSE c%u",
-				 fsstate->cursor_number);
+		reinitialize_scan = true;
 	}
 	else if (fsstate->fetch_ct_2 > 1)
 	{
-		if (PQserverVersion(fsstate->conn) < 150000)
+		if (!fsstate->streaming_fetch && PQserverVersion(fsstate->conn) < 150000)
+		{
+			drain_other_active_scan(fsstate->conn_state, fsstate->conn, fsstate);
 			snprintf(sql, sizeof(sql), "MOVE BACKWARD ALL IN c%u",
 					 fsstate->cursor_number);
+			res = pgfdw_exec_query(fsstate->conn, sql, fsstate->conn_state);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				pgfdw_report_error(res, fsstate->conn, sql);
+
+			PQclear(res);
+
+			/* Now force a fresh FETCH. */
+			fsstate->tuples = NULL;
+			fsstate->num_tuples = 0;
+			fsstate->next_tuple = 0;
+			fsstate->fetch_ct_2 = 0;
+			fsstate->eof_reached = false;
+			return;
+		}
+		else
+			reinitialize_scan = true;
+	}
+	else
+	{
+		/*
+		 * Easy: just rescan what we already have in memory, if anything.
+		 *
+		 * Exception: in streaming_fetch mode a populated tuplestore holds
+		 * rows drained from a previous pass; those cannot be rewound to the
+		 * start, so we must reinitialise the scan from scratch.
+		 */
+		if (fsstate->streaming_fetch && fsstate->tuplestore)
+			reinitialize_scan = true;
 		else
 		{
-			fsstate->scan_in_progress = false;
-			snprintf(sql, sizeof(sql), "CLOSE c%u",
-					 fsstate->cursor_number);
+			fsstate->next_tuple = 0;
+			return;
 		}
 	}
-	else
+	if (reinitialize_scan)
 	{
-		/* Easy: just rescan what we already have in memory, if anything */
+		if (fsstate->streaming_fetch)
+		{
+			if (is_active_scan(fsstate))
+			{
+				fsstate->conn_state->active_scan = NULL;
+				pgfdw_cancel_query(fsstate->conn);
+			}
+			if (fsstate->tuplestore)
+			{
+				tuplestore_end(fsstate->tuplestore);
+				fsstate->tuplestore = NULL;
+			}
+		}
+		else
+		{
+			drain_other_active_scan(fsstate->conn_state, fsstate->conn, fsstate);
+			snprintf(sql, sizeof(sql), "CLOSE c%u",
+					 fsstate->cursor_number);
+			res = pgfdw_exec_query(fsstate->conn, sql, fsstate->conn_state);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				pgfdw_report_error(res, fsstate->conn, sql);
+			PQclear(res);
+		}
+		/* Now force a fresh FETCH. */
+		fsstate->tuples = NULL;
+		fsstate->num_tuples = 0;
 		fsstate->next_tuple = 0;
-		return;
+		fsstate->fetch_ct_2 = 0;
+		fsstate->eof_reached = false;
+		fsstate->scan_in_progress = false;
 	}
-
-	res = pgfdw_exec_query(fsstate->conn, sql, fsstate->conn_state);
-	if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		pgfdw_report_error(res, fsstate->conn, sql);
-	PQclear(res);
-
-	/* Now force a fresh FETCH. */
-	fsstate->tuples = NULL;
-	fsstate->num_tuples = 0;
-	fsstate->next_tuple = 0;
-	fsstate->fetch_ct_2 = 0;
-	fsstate->eof_reached = false;
 }
 
 /*
@@ -1824,9 +1896,27 @@ postgresEndForeignScan(ForeignScanState *node)
 
 	/* Close the cursor if open, to prevent accumulation of cursors */
 	if (fsstate->scan_in_progress)
-		close_cursor(fsstate->conn, fsstate->cursor_number,
-					 fsstate->conn_state);
+	{
+		if (fsstate->streaming_fetch)
+		{
+			/* Remove the pointer from conn_state since ending this scan. */
+			if (is_active_scan(fsstate))
+			{
+				fsstate->conn_state->active_scan = NULL;
+				pgfdw_cancel_query(fsstate->conn);
+			}
+			if (fsstate->tuplestore)
+				tuplestore_end(fsstate->tuplestore);
 
+			MemoryContextReset(fsstate->batch_cxt);
+		}
+		else
+		{
+			drain_other_active_scan(fsstate->conn_state, fsstate->conn, fsstate);
+			close_cursor(fsstate->conn, fsstate->cursor_number,
+						 fsstate->conn_state);
+		}
+	}
 	/* Release remote connection */
 	ReleaseConnection(fsstate->conn);
 	fsstate->conn = NULL;
@@ -3017,9 +3107,13 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
 	if (es->verbose)
 	{
 		char	   *sql;
+		bool		stream_fetch;
 
 		sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql));
+		stream_fetch = boolVal(list_nth(fdw_private, FdwScanPrivateStreamingFetch));
 		ExplainPropertyText("Remote SQL", sql, es);
+		if (stream_fetch)
+			ExplainPropertyBool("streaming_fetch", stream_fetch, es);
 	}
 }
 
@@ -3830,6 +3924,12 @@ create_cursor(ForeignScanState *node)
 	if (fsstate->conn_state->pendingAreq)
 		process_pending_request(fsstate->conn_state->pendingAreq);
 
+	/*
+	 * If the other scan_in_progress is using streaming_fetch mode, then save
+	 * the tuples to tuplestore before creating this cursor.
+	 */
+	drain_other_active_scan(fsstate->conn_state, fsstate->conn, fsstate);
+
 	/*
 	 * Construct array of query parameter values in text format.  We do the
 	 * conversions in the short-lived per-tuple context, so as not to cause a
@@ -3885,6 +3985,70 @@ create_cursor(ForeignScanState *node)
 	pfree(buf.data);
 }
 
+static void
+init_scan(ForeignScanState *node)
+{
+	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+	ExprContext *econtext = node->ss.ps.ps_ExprContext;
+	int			numParams = fsstate->numParams;
+	const char **values = fsstate->param_values;
+	PGconn	   *conn = fsstate->conn;
+
+	/* First, process a pending asynchronous request, if any. */
+	if (fsstate->conn_state->pendingAreq)
+		process_pending_request(fsstate->conn_state->pendingAreq);
+
+	/*
+	 * Construct array of query parameter values in text format.  We do the
+	 * conversions in the short-lived per-tuple context, so as not to cause a
+	 * memory leak over repeated scans.
+	 */
+	if (numParams > 0)
+	{
+		MemoryContext oldcontext;
+
+		oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+
+		process_query_params(econtext,
+							 fsstate->param_flinfo,
+							 fsstate->param_exprs,
+							 values);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/*
+	 * Finish fetching tuples of the last query.
+	 */
+	drain_other_active_scan(fsstate->conn_state, fsstate->conn, fsstate);
+
+	if (!PQsendQueryParams(conn, fsstate->query, numParams,
+						   NULL, values, NULL, NULL, 0))
+		pgfdw_report_error(NULL, conn, fsstate->query);
+
+	/*
+	 * Call for Chunked rows mode with same size of chunk as the fetch size
+	 */
+	if (!PQsetChunkedRowsMode(conn, fsstate->fetch_size))
+	{
+		pgfdw_cancel_query(fsstate->conn);
+		pgfdw_report_error(NULL, conn, fsstate->query);
+	}
+
+	fsstate->tuples = NULL;
+	fsstate->num_tuples = 0;
+	fsstate->next_tuple = 0;
+	fsstate->fetch_ct_2 = 0;
+	fsstate->eof_reached = false;
+
+	/*
+	 * To remember the current scan as the last one, when control switches to
+	 * another scan
+	 */
+	fsstate->conn_state->active_scan = fsstate;
+	fsstate->scan_in_progress = true;
+}
+
 /*
  * Fetch some more rows from the node's cursor.
  */
@@ -3894,14 +4058,10 @@ fetch_more_data(ForeignScanState *node)
 	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
 	PGconn	   *conn = fsstate->conn;
 	PGresult   *res;
-	int			numrows;
-	int			i;
+	int			numrows = 0,
+				i = 0;
 	MemoryContext oldcontext;
 
-	/*
-	 * We'll store the tuples in the batch_cxt.  First, flush the previous
-	 * batch.
-	 */
 	fsstate->tuples = NULL;
 	MemoryContextReset(fsstate->batch_cxt);
 	oldcontext = MemoryContextSwitchTo(fsstate->batch_cxt);
@@ -3922,7 +4082,7 @@ fetch_more_data(ForeignScanState *node)
 		/* Reset per-connection state */
 		fsstate->conn_state->pendingAreq = NULL;
 	}
-	else
+	else if (!fsstate->streaming_fetch)
 	{
 		char		sql[64];
 
@@ -3935,7 +4095,50 @@ fetch_more_data(ForeignScanState *node)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pgfdw_report_error(res, conn, fsstate->query);
 	}
+	else
+	{
+		/*
+		 * When cursor mode is not used, there is a special possibility --
+		 * reading from the tuplestore. For processing of tuples nothing
+		 * changes in cursor and mode without cursor during fetch.
+		 */
+		if (fsstate->tuplestore)
+		{
+			/* Reading tuples from tuplestore */
+			fetch_from_tuplestore(node);
+			MemoryContextSwitchTo(oldcontext);
+			return;
+		}
 
+		/*
+		 * Non-cursor mode uses PQSetChunkedRowsMode during init_scan, so just
+		 * get the result here.
+		 */
+		res = pgfdw_get_next_result(conn);
+
+		if (!res || PQresultStatus(res) == PGRES_FATAL_ERROR)
+			pgfdw_report_error(res, conn, fsstate->query);
+
+		/*
+		 * PGRES_TUPLES_OK is the end-of-stream sentinel in chunked mode.
+		 * Consume the trailing protocol NULL now and clear active_scan so a
+		 * concurrent init_scan does not call save_to_tuplestore on an
+		 * already-idle connection.
+		 */
+		if (PQresultStatus(res) == PGRES_TUPLES_OK)
+		{
+			Assert(PQntuples(res) == 0);
+			PQclear(res);
+			res = pgfdw_get_next_result(conn);
+			if (res != NULL)
+				pgfdw_report_error(res, conn, fsstate->query);
+			fsstate->conn_state->active_scan = NULL;
+			fsstate->eof_reached = true;
+			MemoryContextSwitchTo(oldcontext);
+			return;
+		}
+
+	}
 	/* Convert the data into HeapTuples */
 	numrows = PQntuples(res);
 	fsstate->tuples = (HeapTuple *) palloc0(numrows * sizeof(HeapTuple));
@@ -3964,9 +4167,166 @@ fetch_more_data(ForeignScanState *node)
 
 	PQclear(res);
 
+	/*
+	 * In streaming_fetch mode, a partial chunk signals EOF, but TUPLES_OK and
+	 * the protocol NULL are still pending.  Drain them now so the connection
+	 * is immediately reusable and active_scan is cleared.
+	 */
+	if (fsstate->streaming_fetch && fsstate->eof_reached)
+	{
+		PGresult   *tail = pgfdw_get_next_result(conn);
+
+		if (tail && PQresultStatus(tail) == PGRES_TUPLES_OK)
+		{
+			PQclear(tail);
+			tail = pgfdw_get_next_result(conn);
+		}
+		if (tail != NULL)
+			pgfdw_report_error(tail, conn, fsstate->query);
+
+		fsstate->conn_state->active_scan = NULL;
+	}
+
 	MemoryContextSwitchTo(oldcontext);
 }
 
+static bool
+is_active_scan(PgFdwScanState *fsstate)
+{
+	if (fsstate->streaming_fetch && fsstate->conn_state->active_scan &&
+		fsstate == fsstate->conn_state->active_scan)
+		return true;
+	return false;
+}
+
+/*
+ * This is used in streaming_fetch mode only to fetch the tuples from the tuplestore.
+ */
+static void
+fetch_from_tuplestore(ForeignScanState *node)
+{
+	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+	TupleTableSlot *slot;
+	int			numrows = 0,
+				i = 0;
+
+	/* Retrieve the tuples from the tuplestore instead of actual fetch */
+	numrows = tuplestore_tuple_count(fsstate->tuplestore);
+	fsstate->tuples = (HeapTuple *) palloc0(numrows * sizeof(HeapTuple));
+	slot = MakeSingleTupleTableSlot(fsstate->tupdesc, &TTSOpsMinimalTuple);
+
+	while (tuplestore_gettupleslot(fsstate->tuplestore, true, true, slot))
+	{
+		fsstate->tuples[i++] = ExecFetchSlotHeapTuple(slot, true, NULL);
+		ExecClearTuple(slot);
+	}
+	fsstate->num_tuples = numrows;
+	fsstate->next_tuple = 0;
+	fsstate->eof_reached = true;
+
+	/* Clean up */
+	tuplestore_end(fsstate->tuplestore);
+	ExecDropSingleTupleTableSlot(slot);
+	fsstate->tuplestore = NULL;
+}
+
+/*
+ * If another scan on this connection is a streaming_fetch scan that still
+ * has results in flight, drain them into its tuplestore before we send any
+ * command of our own — otherwise PQsendQuery() will fail because the
+ * connection already has an outstanding, unconsumed result.
+ */
+static void
+drain_other_active_scan(PgFdwConnState *conn_state, PGconn *conn,
+						PgFdwScanState *self)
+{
+	if (conn_state->active_scan && conn_state->active_scan != self)
+		save_to_tuplestore(conn_state->active_scan, conn);
+}
+
+/*
+ * In non-cursor mode only, save the tuples to tuplestore.
+ */
+static void
+save_to_tuplestore(PgFdwScanState *active_fsstate, PGconn *conn)
+{
+	PGresult   *res = NULL;
+	int			numrows = 0,
+				i = 0;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(MemoryContextGetParent(active_fsstate->batch_cxt));
+
+	res = pgfdw_get_next_result(conn);
+
+	if (!res || PQresultStatus(res) == PGRES_FATAL_ERROR)
+		pgfdw_report_error(res, conn, active_fsstate->query);
+	else if (PQresultStatus(res) == PGRES_TUPLES_OK)
+	{
+		/*
+		 * This signifies there is nothing todo, all is fetched. But this
+		 * happened only after this call, so set the eof_reached for the scan.
+		 */
+		active_fsstate->eof_reached = true;
+		Assert(PQntuples(res) == 0);
+		PQclear(res);
+	}
+	else if (PQresultStatus(res) == PGRES_TUPLES_CHUNK)
+	{
+		active_fsstate->tuplestore = tuplestore_begin_heap(true, false, work_mem);
+
+		for (;;)
+		{
+			/*
+			 * Since it is using PQSetChunkedRowsMode, we only get the
+			 * fsstate->fetch_size tuples in one run, so keep on executing
+			 * till we get NULL in PGresult i.e. all the tuples are retrieved.
+			 */
+			CHECK_FOR_INTERRUPTS();
+			numrows = PQntuples(res);
+
+			/* Convert the data into HeapTuples */
+			for (i = 0; i < numrows; i++)
+			{
+				HeapTuple	temp_tuple;
+
+				temp_tuple = make_tuple_from_result_row(res, i,
+														active_fsstate->rel,
+														active_fsstate->attinmeta,
+														active_fsstate->retrieved_attrs,
+														active_fsstate->fsnode,
+														active_fsstate->temp_cxt);
+				tuplestore_puttuple(active_fsstate->tuplestore, temp_tuple);
+				heap_freetuple(temp_tuple);
+			}
+			PQclear(res);
+			res = pgfdw_get_next_result(conn);
+			if (res == NULL || PQresultStatus(res) == PGRES_FATAL_ERROR)
+				pgfdw_report_error(res, conn, active_fsstate->query);
+			else if (PQresultStatus(res) == PGRES_TUPLES_OK)
+			{
+				Assert(PQntuples(res) == 0);
+				PQclear(res);
+				break;
+			}
+		}
+	}
+
+	/*
+	 * There are no more tuples to fetch, a final call to reach the terminal
+	 * state.
+	 */
+	res = pgfdw_get_next_result(conn);
+	Assert(res == NULL);
+	MemoryContextSwitchTo(oldcontext);
+
+	/*
+	 * The connection is now idle again; let other scans on this connection
+	 * resume using it.
+	 */
+	active_fsstate->conn_state->active_scan = NULL;
+}
+
 /*
  * Force assorted GUC parameters to settings that ensure that we'll output
  * data values in a form that is unambiguous to the remote server.
@@ -4236,6 +4596,12 @@ execute_foreign_modify(EState *estate,
 	/* Convert parameters needed by prepared statement to text form */
 	p_values = convert_prep_stmt_params(fmstate, ctid, slots, *numSlots);
 
+	/*
+	 * If the other scan_in_progress is using streaming_fetch mode, then save
+	 * the tuples to tuplestore before proceeding further.
+	 */
+	drain_other_active_scan(fmstate->conn_state, fmstate->conn, NULL);
+
 	/*
 	 * Execute the prepared statement.
 	 */
@@ -4626,6 +4992,12 @@ execute_dml_stmt(ForeignScanState *node)
 							 dmstate->param_exprs,
 							 values);
 
+	/*
+	 * If the other scan_in_progress is using streaming_fetch mode, then save
+	 * the tuples to tuplestore before proceeding further.
+	 */
+	drain_other_active_scan(dmstate->conn_state, dmstate->conn, NULL);
+
 	/*
 	 * Notice that we pass NULL for paramTypes, thus forcing the remote server
 	 * to infer types for all parameters.  Since we explicitly cast every
@@ -5334,7 +5706,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 	for (;;)
 	{
 		int			numrows;
-		int			i;
+		int			i = 0;
 
 		/* Allow users to cancel long query */
 		CHECK_FOR_INTERRUPTS();
@@ -7043,6 +7415,8 @@ apply_server_options(PgFdwRelationInfo *fpinfo)
 			(void) parse_int(defGetString(def), &fpinfo->fetch_size, 0, NULL);
 		else if (strcmp(def->defname, "async_capable") == 0)
 			fpinfo->async_capable = defGetBoolean(def);
+		else if (strcmp(def->defname, "streaming_fetch") == 0)
+			set_streaming_fetch(def, fpinfo);
 	}
 }
 
@@ -7066,9 +7440,20 @@ apply_table_options(PgFdwRelationInfo *fpinfo)
 			(void) parse_int(defGetString(def), &fpinfo->fetch_size, 0, NULL);
 		else if (strcmp(def->defname, "async_capable") == 0)
 			fpinfo->async_capable = defGetBoolean(def);
+		else if (strcmp(def->defname, "streaming_fetch") == 0)
+			set_streaming_fetch(def, fpinfo);
 	}
 }
 
+static void
+set_streaming_fetch(DefElem *def, PgFdwRelationInfo *fpinfo)
+{
+	fpinfo->streaming_fetch = defGetBoolean(def);
+	if (fpinfo->streaming_fetch && PQlibVersion() < 170000)
+		ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("streaming_fetch requires libpq version 17 or later"));
+}
+
 /*
  * Merge FDW options from input relations into a new set of options for a join
  * or an upper rel.
@@ -7101,6 +7486,7 @@ merge_fdw_options(PgFdwRelationInfo *fpinfo,
 	fpinfo->use_remote_estimate = fpinfo_o->use_remote_estimate;
 	fpinfo->fetch_size = fpinfo_o->fetch_size;
 	fpinfo->async_capable = fpinfo_o->async_capable;
+	fpinfo->streaming_fetch = fpinfo_o->streaming_fetch;
 
 	/* Merge the table level options from either side of the join. */
 	if (fpinfo_i)
@@ -7132,6 +7518,11 @@ merge_fdw_options(PgFdwRelationInfo *fpinfo,
 		 */
 		fpinfo->async_capable = fpinfo_o->async_capable ||
 			fpinfo_i->async_capable;
+		fpinfo->streaming_fetch = fpinfo_o->streaming_fetch ||
+			fpinfo_i->streaming_fetch;
+		/* streaming_fetch and async execution are mutually exclusive */
+		if (fpinfo->streaming_fetch)
+			fpinfo->async_capable = false;
 	}
 }
 
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..c096c9dddc2 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -88,6 +88,7 @@ typedef struct PgFdwRelationInfo
 	UserMapping *user;			/* only set in use_remote_estimate mode */
 
 	int			fetch_size;		/* fetch size for this remote table */
+	bool		streaming_fetch;	/* true if cursor-free fetch is enabled */
 
 	/*
 	 * Name of the relation, for use while EXPLAINing ForeignScan.  It is used
@@ -131,12 +132,16 @@ typedef struct PgFdwRelationInfo
 	int			relation_index;
 } PgFdwRelationInfo;
 
+typedef struct PgFdwScanState PgFdwScanState;
+
 /*
  * Extra control information relating to a connection.
  */
 typedef struct PgFdwConnState
 {
 	AsyncRequest *pendingAreq;	/* pending async request */
+	PgFdwScanState *active_scan;	/* last query executed, required for
+									 * non-cursor mode */
 } PgFdwConnState;
 
 /*
@@ -164,8 +169,10 @@ extern unsigned int GetCursorNumber(PGconn *conn);
 extern unsigned int GetPrepStmtNumber(PGconn *conn);
 extern void do_sql_command(PGconn *conn, const char *sql);
 extern PGresult *pgfdw_get_result(PGconn *conn);
+extern PGresult *pgfdw_get_next_result(PGconn *conn);
 extern PGresult *pgfdw_exec_query(PGconn *conn, const char *query,
 								  PgFdwConnState *state);
+extern bool pgfdw_cancel_query(PGconn *conn);
 pg_noreturn extern void pgfdw_report_error(PGresult *res, PGconn *conn,
 										   const char *sql);
 extern void pgfdw_report(int elevel, PGresult *res, PGconn *conn,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index ed2c8b58e60..fac1d069863 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -267,6 +267,243 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1;  -- should work again
 ANALYZE ft1;
 ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true');
 
+-- ===================================================================
+-- test streaming_fetch option
+-- ===================================================================
+CREATE SERVER fetch_stream_srv
+    FOREIGN DATA WRAPPER postgres_fdw
+    OPTIONS (dbname :'current_database', port :'current_port');
+CREATE USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+
+CREATE TABLE local_tbl (id int, val text);
+INSERT INTO local_tbl VALUES (1, 'a'), (2, 'b'), (3, 'c');
+-- 1. streaming_fetch set at SERVER level only
+
+-- 1a. Set streaming_fetch = true on the server
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+
+CREATE FOREIGN TABLE ft_server (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+
+-- Verify option appears in foreign server
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+
+-- Verify option is NOT present at table level (inherits from server)
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+-- 1b. Switch server-level to streaming_fetch = false
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+
+-- Verify option is NOT present at table level (inherits from server)
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+-- 2. streaming_fetch set at TABLE level only (no server-level option)
+ALTER SERVER fetch_stream_srv OPTIONS (DROP streaming_fetch);
+
+-- 2a. streaming_fetch = true at table level
+ALTER FOREIGN TABLE ft_server OPTIONS (ADD streaming_fetch 'true');
+
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+SELECT * FROM ft_server ORDER BY id;
+
+-- 2b. streaming_fetch = false at table level
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+SELECT * FROM ft_server ORDER BY id;
+
+-- 3. TABLE-level value overrides SERVER-level value
+
+-- 3a. Server = true, Table = false
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+
+-- Server  shows true
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+
+-- Table shows its own override: false
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+-- Query must succeed and use table-level setting (false)
+SELECT * FROM ft_server ORDER BY id;
+
+-- 3b. Server = false, Table = true  →  effective value is true
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'true');
+
+-- Server shows false
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch')
+ORDER BY srvname, option_name;
+
+-- Table shows its own override: true
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_server'::regclass;
+
+-- Query must succeed and use table-level setting (true)
+SELECT * FROM ft_server ORDER BY id;
+
+-- 4. Negative tests: invalid values must be rejected
+
+-- streaming_fetch is boolean; non-boolean value must ERROR
+\set VERBOSITY terse
+
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch 'yes');  -- ERROR
+
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch '1');    -- ERROR
+
+CREATE FOREIGN TABLE ft_invalid (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl', streaming_fetch '');     -- ERROR
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'notabool');             -- ERROR
+
+\set VERBOSITY default
+
+-- 5. ALTER FOREIGN TABLE: add, change, and drop streaming_fetch
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'true');
+
+CREATE FOREIGN TABLE ft_alter_test (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+
+-- No table-level option yet
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+
+-- ADD table-level option
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (ADD streaming_fetch 'false');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+
+SELECT * FROM ft_alter_test ORDER BY id;
+
+-- SET (change) table-level option
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (SET streaming_fetch 'true');
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+
+SELECT * FROM ft_alter_test ORDER BY id;
+
+-- DROP table-level option (falls back to server-level)
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (DROP streaming_fetch);
+SELECT ftrelid::regclass, ftoptions
+FROM pg_foreign_table
+WHERE ftrelid = 'ft_alter_test'::regclass;
+
+SELECT * FROM ft_alter_test ORDER BY id;
+
+DROP FOREIGN TABLE ft_alter_test;
+
+-- 6. streaming_fetch with non-default fetch_size values
+-- Use a 12-row table so chunk boundaries are distinct and predictable:
+-- fetch_size=1 gives 12 single-row chunks, fetch_size=5 gives chunks
+-- of 5+5+2, and fetch_size=1000 puts all rows in a single chunk.
+CREATE TABLE local_tbl_large (id int, val text);
+INSERT INTO local_tbl_large SELECT id, 'val' || id FROM generate_series(1, 12) id;
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'true');
+
+-- fetch_size = 1: every row is its own libpq chunk; exercises the path
+-- where pgfdw_get_next_result is called once per row.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '1');
+SELECT count(*) FROM ft_fetchsize;
+DROP FOREIGN TABLE ft_fetchsize;
+
+-- fetch_size = 5: three chunks with a partial last chunk (5+5+2);
+-- the final chunk is smaller than fetch_size.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '5');
+SELECT count(*) FROM ft_fetchsize;
+SELECT * FROM ft_fetchsize ORDER BY id;
+DROP FOREIGN TABLE ft_fetchsize;
+
+-- fetch_size exceeds the table row count: all rows arrive in one chunk
+-- followed immediately by the final empty PGRES_TUPLES_OK result.
+CREATE FOREIGN TABLE ft_fetchsize (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl_large', fetch_size '1000');
+SELECT count(*) FROM ft_fetchsize;
+DROP FOREIGN TABLE ft_fetchsize;
+
+DROP TABLE local_tbl_large;
+
+-- 7. streaming_fetch combined with use_remote_estimate
+-- use_remote_estimate issues a remote EXPLAIN to size the scan at plan
+-- time; streaming_fetch must not interfere with that EXPLAIN call.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD use_remote_estimate 'true');
+
+CREATE FOREIGN TABLE ft_remote_est (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+
+-- Verify both options are active at the server level.
+SELECT srvname, option_name, option_value
+FROM pg_foreign_server,
+     LATERAL pg_options_to_table(srvoptions)
+WHERE srvname = 'fetch_stream_srv'
+  AND option_name IN ('streaming_fetch', 'use_remote_estimate')
+ORDER BY option_name;
+
+-- Both options active: use_remote_estimate sizes the scan remotely via
+-- EXPLAIN, then streaming_fetch fetches rows without a cursor.
+SELECT * FROM ft_remote_est ORDER BY id;
+
+ALTER SERVER fetch_stream_srv OPTIONS (DROP use_remote_estimate);
+
+-- 9. streaming_fetch check the output in explain with verbose
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft_remote_est ORDER BY id;
+
+-- Cleanup
+DROP FOREIGN TABLE ft_remote_est;
+DROP USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+DROP SERVER fetch_stream_srv CASCADE;
+DROP TABLE local_tbl;
 -- ===================================================================
 -- test subscription
 -- ===================================================================
@@ -307,6 +544,32 @@ SELECT COUNT(*) FROM ft1 t1;
 SELECT * FROM ft1 t1 WHERE t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 <= 10) ORDER BY c1;
 -- subquery+MAX
 SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+-- Test in streaming_fetch mode to cover path from subquery params
+-- with only one table using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (streaming_fetch 'true');
+SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+-- Test join with only one table using streaming_fetch at a time
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (streaming_fetch 'true');
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+-- with both the tables using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1;
+-- Test join with both the tables using streaming_fetch
+SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- streaming_fetch: verify correct results when parallel-friendly settings
+-- are active locally.  With no cursor on the remote side, the remote
+-- planner is free to choose a parallel plan; results must match exactly.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SET max_parallel_workers_per_gather = 2;
+SET min_parallel_table_scan_size = 0;
+SELECT count(*) FROM ft1;
+RESET max_parallel_workers_per_gather;
+RESET min_parallel_table_scan_size;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
 -- used in CTE
 WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1;
 -- fixed values
@@ -386,6 +649,54 @@ EXPLAIN (VERBOSE, COSTS OFF)
   WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
 SELECT * FROM ft2 a, ft2 b
 WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+
+-- Test in streaming_fetch mode for rescan path
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+-- Test for streaming_fetch covering rescans and three active cursors
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1 AND a.c8 = 'foo'
+AND b.c7 = upper(a.c7);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+-- Test in streaming_fetch mode for interleaved scans.
+-- The non-shippable condition a.c8 = 'foo' (user_enum is not a built-in type)
+-- prevents full join pushdown, so the planner issues two separate FDW scans
+-- that share the same loopback connection.  When the inner scan's init_scan
+-- fires it calls save_to_tuplestore to drain the outer scan's unread rows.
+ ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+
+-- Show the plan: ft2 must appear as two independent ForeignScan nodes, not
+-- a single pushed-down remote join.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+
+-- Verify results with cursor path.
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+
+-- Three-way self-join to test streaming_fetch
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+
+-- output matches in cursor mode
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+SELECT count(*) FROM ft2 a, ft2 b, ft2 c
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND c.c1 = b.c1
+AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+
 -- bug before 9.3.5 due to sloppy handling of remote-estimate parameters
 SELECT * FROM ft1 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft2 WHERE c1 < 5));
 SELECT * FROM ft2 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft1 WHERE c1 < 5));
@@ -676,6 +987,16 @@ SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1)
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
 SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+
+-- Test in streaming_fetch mode to cover the patch for two simultaneous active cursors
+-- with only one table using streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+-- with both the tables using streaming_fetch
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
 -- CROSS JOIN can be pushed down
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
@@ -684,6 +1005,12 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 1
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
 SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+-- Test in streaming_fetch mode to cover the case with multiple cursors but only one active cursor at a time
+ALTER FOREIGN TABLE ft5 OPTIONS (streaming_fetch 'true');
+ALTER FOREIGN TABLE ft6 OPTIONS (streaming_fetch 'true');
+SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ALTER FOREIGN TABLE ft5 OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft6 OPTIONS (SET streaming_fetch 'false');
 -- unsafe join conditions (c8 has a UDT), not pushed down. Practically a CROSS
 -- JOIN since c8 in both tables has same value.
 EXPLAIN (VERBOSE, COSTS OFF)
@@ -831,6 +1158,31 @@ explain (verbose, costs off)
 select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2 limit 1;
 select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2 limit 1;
 
+-- Test with limit and streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+select count(c6), sum(c1), avg(c1), min(c2), max(c1), stddev(c2), sum(c1) * (random() <= 1)::int as sum2 from ft1 where c2 < 5 group by c2 order by 1, 2 limit 1;
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+
+-- Test LIMIT stopping before all tuples are consumed.
+-- The WHERE clause references c8 (a user-defined type that cannot be
+-- pushed to the remote), preventing LIMIT pushdown.  The remote
+-- therefore streams all rows, and end_scan must discard the in-flight
+-- data when the local executor stops early.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+-- LIMIT 5 with default fetch_size=100: stops well within the first
+-- chunk; end_scan discards ~995 rows still in flight on the connection.
+SELECT c1 FROM ft1 WHERE c8 = 'foo' ORDER BY c1 LIMIT 5;
+-- Verify the connection is still usable after the early stop.
+SELECT count(*) FROM ft1;
+-- fetch_size=10, LIMIT=15: consumes one full chunk (rows 1-10) plus 5
+-- rows from a second chunk (rows 11-15); end_scan then discards the
+-- remainder of that chunk and all subsequent in-flight chunks.
+ALTER FOREIGN TABLE ft1 OPTIONS (fetch_size '10');
+SELECT c1 FROM ft1 WHERE c8 = 'foo' ORDER BY c1 LIMIT 15;
+SELECT count(*) FROM ft1;
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+
 -- Aggregate is not pushed down as aggregation contains random()
 explain (verbose, costs off)
 select sum(c1 * (random() <= 1)::int) as sum, avg(c1) from ft1;
@@ -3600,6 +3952,41 @@ SELECT 1 FROM ft1 LIMIT 1;    -- should fail
 \set VERBOSITY default
 COMMIT;
 
+-- ===================================================================
+-- streaming_fetch: error recovery when the remote backend terminates
+-- ===================================================================
+-- Enable streaming_fetch
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+
+-- Establish a fresh remote connection.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Terminate the remote backend and wait for the termination to complete.
+DO $$ BEGIN
+PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity
+	WHERE application_name = 'fdw_retry_check';
+END $$;
+
+-- After the connection is broken, a streaming_fetch query should detect
+-- the broken connection, reestablish it, and succeed.
+BEGIN;
+SELECT c1 FROM ft1 ORDER BY c1 LIMIT 3;
+
+-- Inside a subtransaction the broken connection must not be silently
+-- retried; the query should fail.
+DO $$ BEGIN
+PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity
+	WHERE application_name = 'fdw_retry_check';
+END $$;
+SAVEPOINT s2;
+-- The text of the error might vary across platforms, so only show SQLSTATE.
+\set VERBOSITY sqlstate
+SELECT 1 FROM ft1 LIMIT 1;    -- should fail
+\set VERBOSITY default
+COMMIT;
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+
 -- =============================================================================
 -- test connection invalidation cases and postgres_fdw_get_connections function
 -- =============================================================================
@@ -4158,6 +4545,29 @@ SELECT * FROM local_tbl, async_pt WHERE local_tbl.a = async_pt.a AND local_tbl.c
 ALTER FOREIGN TABLE async_p1 OPTIONS (DROP use_remote_estimate);
 ALTER FOREIGN TABLE async_p2 OPTIONS (DROP use_remote_estimate);
 
+-- Test with streaming_fetch
+-- No streaming_fetch at server, this should give Async Foreign Scan for for async_p1 and async_p2
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+
+-- streaming_fetch = false at loopback server, this should still give Async Foreign Scan for async_p1 and async_p2
+ALTER SERVER loopback OPTIONS (streaming_fetch 'false');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+
+-- streaming_fetch = false at loopback server but true for async_p1, this should give Foreign Scan for async_p1
+ALTER FOREIGN TABLE async_p1 OPTIONS (ADD streaming_fetch 'true');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+
+-- streaming_fetch = true at loopback2 server, this should give Foreign Scan for async_p2 also
+ALTER SERVER loopback2 OPTIONS (streaming_fetch 'true');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM async_pt;
+
+ALTER FOREIGN TABLE async_p1 OPTIONS (DROP streaming_fetch);
+ALTER SERVER loopback OPTIONS (DROP streaming_fetch);
+ALTER SERVER loopback2 OPTIONS (DROP streaming_fetch);
 DROP TABLE local_tbl;
 DROP INDEX base_tbl1_idx;
 DROP INDEX base_tbl2_idx;
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index b9e1b04463e..4d34baa291b 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -476,6 +476,21 @@ OPTIONS (ADD password_required 'false');
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>streaming_fetch</literal> (<type>boolean</type>)</term>
+     <listitem>
+      <para>
+       Specifies whether to fetch rows from the remote server without
+       using a cursor. When enabled, the remote server is free to use
+       parallel query plans, which can significantly improve performance
+       for large scans. When a second scan begins on the same connection
+       while one is already in progress, the remaining rows of the first scan
+       are buffered locally (using work_mem) and replayed on demand.
+       Asynchronous execution is not supported in this mode and is disabled
+       automatically. The default is <literal>false</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
 
   </sect3>
-- 
2.39.5 (Apple Git-154)

