From 89ae156eb5082f97d929c81d57936978f8b9e49e Mon Sep 17 00:00:00 2001
From: Rafia Sabih <rafia.sabih@cybertec.at>
Date: Wed, 26 Aug 2026 11:34:20 +0200
Subject: [PATCH v16 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             |   75 +-
 .../postgres_fdw/expected/postgres_fdw.out    | 1208 ++++++++++++++++-
 .../postgres_fdw/expected/query_cancel.out    |   23 +
 contrib/postgres_fdw/option.c                 |    4 +
 contrib/postgres_fdw/postgres_fdw.c           |  524 ++++++-
 contrib/postgres_fdw/postgres_fdw.h           |    8 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  475 +++++++
 contrib/postgres_fdw/sql/query_cancel.sql     |   16 +
 doc/src/sgml/postgres-fdw.sgml                |   17 +
 9 files changed, 2242 insertions(+), 108 deletions(-)

diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index b5d4cf3dccc..d705e69b7eb 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -163,11 +163,10 @@ 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,
-								   bool consume_input);
+								   bool consume_input, PgFdwConnState *state);
 static bool pgfdw_exec_cleanup_query(PGconn *conn, const char *query,
 									 bool ignore_errors);
 static bool pgfdw_exec_cleanup_query_begin(PGconn *conn, const char *query);
@@ -299,6 +298,13 @@ GetConnection(UserMapping *user, bool will_prep_stmt, PgFdwConnState **state)
 		/* Process a pending asynchronous request if any. */
 		if (entry->state.pendingAreq)
 			process_pending_request(entry->state.pendingAreq);
+
+		/*
+		 * If some other scan on this connection is using streaming_fetch and
+		 * still has an unconsumed result, drain it too, so the caller gets an
+		 * idle connection back.
+		 */
+		drain_other_active_scan(&entry->state);
 		/* Start a new transaction or subtransaction if needed. */
 		begin_remote_xact(entry);
 	}
@@ -1069,14 +1075,26 @@ GetPrepStmtNumber(PGconn *conn)
  * ignore that for now.
  *
  * Caller is responsible for the error handling on the result.
+ *
+ * Every caller must pass the PgFdwConnState for the connection (not NULL),
+ * so that if some other streaming_fetch scan on this connection still has
+ * an unconsumed result pending, we can drain it before sending a new query
+ * down the same connection.
  */
 PGresult *
 pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state)
 {
 	/* First, process a pending asynchronous request, if any. */
-	if (state && state->pendingAreq)
+	if (state->pendingAreq)
 		process_pending_request(state->pendingAreq);
 
+	/*
+	 * If some other scan on this connection is using streaming_fetch and
+	 * still has an unconsumed result, drain it now so the connection is idle
+	 * before we send a new query.
+	 */
+	drain_other_active_scan(state);
+
 	if (!PQsendQuery(conn, query))
 		return NULL;
 	return pgfdw_get_result(conn);
@@ -1093,6 +1111,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.
  *
@@ -1243,7 +1271,7 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 					if (entry->have_prep_stmt && entry->have_error)
 					{
 						res = pgfdw_exec_query(entry->conn, "DEALLOCATE ALL",
-											   NULL);
+											   &entry->state);
 						PQclear(res);
 					}
 					entry->have_prep_stmt = false;
@@ -1569,9 +1597,10 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel)
  * XXX: if the query was one sent by fetch_more_data_begin(), we could get the
  * query text from the pendingAreq saved in the per-connection state, then
  * report the query using it.
+ * On success, also clears conn_state->active_scan via call to pgfdw_cancel_query_end.
  */
-static bool
-pgfdw_cancel_query(PGconn *conn)
+bool
+pgfdw_cancel_query(PGconn *conn, PgFdwConnState *state)
 {
 	TimestampTz now = GetCurrentTimestamp();
 	TimestampTz endtime;
@@ -1591,7 +1620,7 @@ pgfdw_cancel_query(PGconn *conn)
 
 	if (!pgfdw_cancel_query_begin(conn, endtime))
 		return false;
-	return pgfdw_cancel_query_end(conn, endtime, retrycanceltime, false);
+	return pgfdw_cancel_query_end(conn, endtime, retrycanceltime, false, state);
 }
 
 /*
@@ -1618,7 +1647,8 @@ pgfdw_cancel_query_begin(PGconn *conn, TimestampTz endtime)
 
 static bool
 pgfdw_cancel_query_end(PGconn *conn, TimestampTz endtime,
-					   TimestampTz retrycanceltime, bool consume_input)
+					   TimestampTz retrycanceltime, bool consume_input,
+					   PgFdwConnState *state)
 {
 	PGresult   *result;
 	bool		timed_out;
@@ -1654,6 +1684,8 @@ pgfdw_cancel_query_end(PGconn *conn, TimestampTz endtime,
 		return false;
 	}
 	PQclear(result);
+	/* Clear the active_scan */
+	state->active_scan = NULL;
 
 	return true;
 }
@@ -1906,7 +1938,7 @@ pgfdw_abort_cleanup(ConnCacheEntry *entry, bool toplevel)
 	 * remote server, and if so, request cancellation of the command.
 	 */
 	if (PQtransactionStatus(entry->conn) == PQTRANS_ACTIVE &&
-		!pgfdw_cancel_query(entry->conn))
+		!pgfdw_cancel_query(entry->conn, &entry->state))
 		return;					/* Unable to cancel running query */
 
 	CONSTRUCT_ABORT_COMMAND(sql, entry, toplevel);
@@ -1929,10 +1961,15 @@ pgfdw_abort_cleanup(ConnCacheEntry *entry, bool toplevel)
 	 * If pendingAreq of the per-connection state is not NULL, it means that
 	 * 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.
+	 * fetch_more_data().  Likewise, active_scan may still be pointing at a
+	 * streaming_fetch scan whose unconsumed result was never drained.  The
+	 * abort we just sent has already discarded any such state on the remote
+	 * side, and the (sub)transaction that owned that scan is going away, so
+	 * reset both unconditionally here rather than leave a dangling pointer
+	 * for the connection's next user.
 	 */
-	if (entry->state.pendingAreq)
-		memset(&entry->state, 0, sizeof(entry->state));
+	entry->state.pendingAreq = NULL;
+	entry->state.active_scan = NULL;
 
 	/* Disarm changing_xact_state if it all worked */
 	entry->changing_xact_state = false;
@@ -2151,7 +2188,7 @@ pgfdw_finish_abort_cleanup(List *pending_entries, List *cancel_requested,
 														  RETRY_CANCEL_TIMEOUT);
 
 			if (!pgfdw_cancel_query_end(entry->conn, endtime,
-										retrycanceltime, true))
+										retrycanceltime, true, &entry->state))
 			{
 				/* Unable to cancel running query */
 				pgfdw_reset_xact_state(entry, toplevel);
@@ -2221,9 +2258,9 @@ 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)
-			memset(&entry->state, 0, sizeof(entry->state));
+		/* Reset the async request and any undrained streaming_fetch scan */
+		entry->state.pendingAreq = NULL;
+		entry->state.active_scan = NULL;
 
 		/* We're done with this entry; unset the changing_xact_state flag */
 		entry->changing_xact_state = false;
@@ -2266,9 +2303,9 @@ pgfdw_finish_abort_cleanup(List *pending_entries, List *cancel_requested,
 		entry->have_prep_stmt = false;
 		entry->have_error = false;
 
-		/* Reset the per-connection state if needed */
-		if (entry->state.pendingAreq)
-			memset(&entry->state, 0, sizeof(entry->state));
+		/* Reset the async request and any undrained streaming_fetch scan */
+		entry->state.pendingAreq = NULL;
+		entry->state.active_scan = NULL;
 
 		/* We're done with this entry; unset the changing_xact_state flag */
 		entry->changing_xact_state = false;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 517d15cf1fa..98caf30c39a 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -277,6 +277,265 @@ 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');
+CREATE FOREIGN TABLE ft_server (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+-- Combined view of the option at both levels, used by the checks below.
+CREATE VIEW streaming_fetch_opt AS
+SELECT (SELECT option_value FROM pg_foreign_server,
+               LATERAL pg_options_to_table(srvoptions)
+        WHERE srvname = 'fetch_stream_srv'
+          AND option_name = 'streaming_fetch') AS server_val,
+       (SELECT ftoptions FROM pg_foreign_table
+        WHERE ftrelid = 'ft_server'::regclass) AS table_opts;
+-- 1. streaming_fetch set at SERVER level only: value tracks the server,
+-- and (being unset) never shows up in the table's own ftoptions.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;
+ server_val |                table_opts                 
+------------+-------------------------------------------
+ true       | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;
+ server_val |                table_opts                 
+------------+-------------------------------------------
+ false      | {schema_name=public,table_name=local_tbl}
+(1 row)
+
+-- 2. streaming_fetch set at TABLE level only (no server-level option);
+-- query results must be correct for both true and false.
+ALTER SERVER fetch_stream_srv OPTIONS (DROP streaming_fetch);
+ALTER FOREIGN TABLE ft_server OPTIONS (ADD streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;
+ server_val |                           table_opts                           
+------------+----------------------------------------------------------------
+            | {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)
+
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;
+ server_val |                           table_opts                            
+------------+-----------------------------------------------------------------
+            | {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; query must use the
+-- effective (table-level) value in both directions.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;  -- server=true, table overrides to false
+ server_val |                           table_opts                            
+------------+-----------------------------------------------------------------
+ true       | {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)
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;  -- server=false, table overrides to true
+ server_val |                           table_opts                           
+------------+----------------------------------------------------------------
+ false      | {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)
+
+DROP VIEW streaming_fetch_opt;
+-- 4. Negative tests: invalid values must be rejected, at both table and
+-- server level.
+\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
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch '1');       -- ERROR
+ERROR:  streaming_fetch requires a Boolean value
+ALTER SERVER fetch_stream_srv OPTIONS (SET 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 ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+                 ftoptions                 
+-------------------------------------------
+ {schema_name=public,table_name=local_tbl}
+(1 row)
+
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (ADD streaming_fetch 'false');
+SELECT ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+                            ftoptions                            
+-----------------------------------------------------------------
+ {schema_name=public,table_name=local_tbl,streaming_fetch=false}
+(1 row)
+
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (SET streaming_fetch 'true');
+SELECT ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+                           ftoptions                            
+----------------------------------------------------------------
+ {schema_name=public,table_name=local_tbl,streaming_fetch=true}
+(1 row)
+
+-- DROP table-level option (falls back to server-level), and confirm the
+-- fallback value is functionally correct.
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (DROP streaming_fetch);
+SELECT ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+                 ftoptions                 
+-------------------------------------------
+ {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;
+-- 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)
+
+-- fetch_size = 5: three chunks with a partial last chunk (5+5+2);
+-- the final chunk is smaller than fetch_size.
+ALTER FOREIGN TABLE ft_fetchsize OPTIONS (SET 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)
+
+-- fetch_size exceeds the table row count: all rows arrive in one chunk
+-- followed immediately by the final empty PGRES_TUPLES_OK result.
+ALTER FOREIGN TABLE ft_fetchsize OPTIONS (SET 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.
+-- ft_server's table-level option is still 'true' from step 3 above.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD use_remote_estimate 'true');
+-- Verify both options are active.
+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.
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft_server ORDER BY id;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Foreign Scan on public.ft_server
+   Output: id, val
+   Remote SQL: SELECT id, val FROM public.local_tbl ORDER BY id ASC NULLS LAST
+   Streaming Fetch: true
+(4 rows)
+
+SELECT * FROM ft_server ORDER BY id;
+ id | val 
+----+-----
+  1 | a
+  2 | b
+  3 | c
+(3 rows)
+
+ALTER SERVER fetch_stream_srv OPTIONS (DROP use_remote_estimate);
+-- Cleanup
+DROP FOREIGN TABLE ft_server;
+DROP USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+DROP SERVER fetch_stream_srv CASCADE;
+DROP TABLE local_tbl;
+-- ===================================================================
 -- test subscription
 -- ===================================================================
 CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1
@@ -448,6 +707,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              
@@ -769,31 +1111,573 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c8 = 'foo';  -- can't be
    Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1"
 (4 rows)
 
--- parameterized remote path for foreign table
+-- parameterized remote path for foreign table
+EXPLAIN (VERBOSE, COSTS OFF)
+  SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2;
+                                                 QUERY PLAN                                                  
+-------------------------------------------------------------------------------------------------------------
+ Nested Loop
+   Output: a."C 1", 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
+   ->  Index Scan using t1_pkey on "S 1"."T 1" a
+         Output: a."C 1", a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8
+         Index Cond: (a."C 1" = 47)
+   ->  Foreign Scan on public.ft2 b
+         Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
+         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
+(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)
+
+-- Test in streaming_fetch mode when a scan that is still the
+-- connection's active_scan (i.e. it has not reached EOF) correctly clears
+-- active_scan.
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+SET statement_timeout = '20s';
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+                                                  QUERY PLAN                                                  
+--------------------------------------------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Output: a.c1, a.c2
+   ->  Foreign Scan on public.ft1 a
+         Output: a.c1, a.c2
+         Filter: (a.c8 = 'foo'::user_enum)
+         Remote SQL: SELECT "C 1", c2, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20)) ORDER BY "C 1" ASC NULLS LAST
+   ->  Foreign Scan on public.ft2 b
+         Output: b.c2
+         Remote SQL: SELECT c2 FROM "S 1"."T 1" WHERE ((c2 = $1::integer))
+         Streaming Fetch: true
+(10 rows)
+
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+ c1 | c2 
+----+----
+  1 |  1
+  2 |  2
+  3 |  3
+  4 |  4
+  5 |  5
+  6 |  6
+  7 |  7
+  8 |  8
+  9 |  9
+ 10 |  0
+ 11 |  1
+ 12 |  2
+ 13 |  3
+ 14 |  4
+ 15 |  5
+ 16 |  6
+ 17 |  7
+ 18 |  8
+ 19 |  9
+(19 rows)
+
+RESET statement_timeout;
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (SET fetch_size '100');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- Verify that aborting a transaction while a streaming_fetch scan still has
+-- an unconsumed chunked result correctly resets active_scan, so the
+-- connection can be reused afterward.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1');
+BEGIN;
+-- Fetch only the first row, leaving the stream unconsumed.
+SELECT c1 FROM ft1 ORDER BY c1 LIMIT 1;
+ c1 
+----
+  1
+(1 row)
+
+SELECT 1/0;  -- force an error, aborting with the scan still active
+ERROR:  division by zero
+ROLLBACK;
+SELECT count(*) FROM ft1;  -- must succeed
+ count 
+-------
+  1000
+(1 row)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+-- Test that fetch_more_data()'s plain cursor-mode FETCH on ft1's own
+-- already-open cursor correctly accounts for another scan's active_scan.
+-- ft1's fetch_size is set below its matching row count, so its cursor
+-- needs a second FETCH; each outer row's EXISTS subplan on ft2 leaves the
+-- inner streaming_fetch scan as the connection's active_scan without
+-- reaching EOF (fetch_size '1' on ft2, and EXISTS only needs one match),
+-- so that second FETCH must drain it first (pgfdw_exec_query() does this
+-- automatically) or ft2's unconsumed chunked result would desync the
+-- connection's result stream.
+ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '5');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+ERROR:  option "fetch_size" provided more than once
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+                                                  QUERY PLAN                                                  
+--------------------------------------------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Output: a.c1, a.c2
+   ->  Foreign Scan on public.ft1 a
+         Output: a.c1, a.c2
+         Filter: (a.c8 = 'foo'::user_enum)
+         Remote SQL: SELECT "C 1", c2, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20)) ORDER BY "C 1" ASC NULLS LAST
+   ->  Foreign Scan on public.ft2 b
+         Output: b.c2
+         Remote SQL: SELECT c2 FROM "S 1"."T 1" WHERE ((c2 = $1::integer))
+         Streaming Fetch: true
+(10 rows)
+
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+ c1 | c2 
+----+----
+  1 |  1
+  2 |  2
+  3 |  3
+  4 |  4
+  5 |  5
+  6 |  6
+  7 |  7
+  8 |  8
+  9 |  9
+ 10 |  0
+ 11 |  1
+ 12 |  2
+ 13 |  3
+ 14 |  4
+ 15 |  5
+ 16 |  6
+ 17 |  7
+ 18 |  8
+ 19 |  9
+(19 rows)
+
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- Test that fetch_from_tuplestore() correctly drains a large backlog in
+-- fetch_size-sized batches rather than all at once: with a small
+-- fetch_size and a self-join matching many rows per outer row, the outer
+-- scan's tuplestore (populated when the inner scan interrupts it to reuse
+-- the connection) needs several batched reads to fully drain.  If a batch
+-- were dropped or duplicated, the count below would not match.
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '5');
+SELECT count(*) FROM ft2 a, ft2 b
+WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
+ count 
+-------
+   100
+(1 row)
+
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- Test that execute_foreign_modify() correctly drains another scan's
+-- undrained streaming_fetch result before sending its own prepared
+-- statement.  The EXISTS subplan against ft2 (streaming_fetch, fetch_size
+-- 1) leaves ft2 as the connection's active_scan without reaching EOF after
+-- each outer row (short-circuiting on the first match), so each matching
+-- row's UPDATE on ft1 -- sent as a separate prepared statement per row,
+-- since the EXISTS subplan prevents direct modify -- must drain it first.
+-- The returned c1 values must match the known-correct row set for this
+-- same EXISTS predicate (see the plain SELECT variant of this query above).
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
 EXPLAIN (VERBOSE, COSTS OFF)
-  SELECT * FROM "S 1"."T 1" a, ft2 b WHERE a."C 1" = 47 AND b.c1 = a.c2;
-                                                 QUERY PLAN                                                  
--------------------------------------------------------------------------------------------------------------
- Nested Loop
-   Output: a."C 1", 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
-   ->  Index Scan using t1_pkey on "S 1"."T 1" a
-         Output: a."C 1", a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8
-         Index Cond: (a."C 1" = 47)
-   ->  Foreign Scan on public.ft2 b
-         Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
-         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = $1::integer))
-(8 rows)
+UPDATE ft1 SET c3 = c3
+WHERE c1 < 20 AND c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = ft1.c2);
+                                                        QUERY PLAN                                                         
+---------------------------------------------------------------------------------------------------------------------------
+ Update on public.ft1
+   Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1
+   ->  Nested Loop Semi Join
+         Output: ft1.c3, ft1.ctid, ft1.*, b.*
+         ->  Foreign Scan on public.ft1
+               Output: ft1.c3, ft1.ctid, ft1.*, ft1.c2
+               Filter: (ft1.c8 = 'foo'::user_enum)
+               Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" < 20)) FOR UPDATE
+         ->  Foreign Scan on public.ft2 b
+               Output: b.*, b.c2
+               Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c2 = $1::integer))
+               Streaming Fetch: true
+(12 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
-(1 row)
+BEGIN;
+UPDATE ft1 SET c3 = c3
+WHERE c1 < 20 AND c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = ft1.c2)
+RETURNING c1;
+ c1 
+----
+  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+(19 rows)
 
--- check both safe and unsafe join conditions
+ROLLBACK;
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+-- Test that execute_dml_stmt() (a fully pushed-down direct-modify
+-- UPDATE/DELETE) also drains another scan's undrained streaming_fetch
+-- result before sending its own query down the shared connection.  ft4
+-- shares the loopback connection with ft2; direct_modify_ft4() performs a
+-- self-contained, direct-modify-eligible UPDATE on ft4, called once per
+-- matching ft1 row while ft2's streaming EXISTS subplan is mid-stream.
+CREATE FUNCTION direct_modify_ft4(id int) RETURNS void AS $$
+  UPDATE ft4 SET c3 = c3 WHERE c1 = id;
+$$ LANGUAGE sql;
+EXPLAIN (VERBOSE, COSTS OFF) UPDATE ft4 SET c3 = c3 WHERE c1 = 1;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Update on public.ft4
+   ->  Foreign Update on public.ft4
+         Remote SQL: UPDATE "S 1"."T 3" SET c3 = c3 WHERE ((c1 = 1))
+(3 rows)
+
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+BEGIN;
+SELECT direct_modify_ft4(a.c1) FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+ direct_modify_ft4 
+-------------------
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+(19 rows)
+
+ROLLBACK;
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+DROP FUNCTION direct_modify_ft4(int);
+-- Test in streaming_fetch mode for interleaved scans.
+-- The non-shippable condition a.c8 = 'foo' prevents full
+-- join pushdown, so the planner issues two separate FDW scans.
+-- When the inner scan is initiated it first drains 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 +1686,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 +1802,53 @@ 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)
+
 -- 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  
@@ -2258,6 +3193,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;
@@ -2309,6 +3281,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)
@@ -3670,6 +4652,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;
@@ -11640,6 +12688,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
 -- =============================================================================
@@ -12869,6 +13959,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/expected/query_cancel.out b/contrib/postgres_fdw/expected/query_cancel.out
index c3fc585d49f..3e539f27fe9 100644
--- a/contrib/postgres_fdw/expected/query_cancel.out
+++ b/contrib/postgres_fdw/expected/query_cancel.out
@@ -32,3 +32,26 @@ SET LOCAL statement_timeout = '10ms';
 SELECT count(*) FROM ft1 a CROSS JOIN ft1 b CROSS JOIN ft1 c CROSS JOIN ft1 d;
 ERROR:  canceling statement due to statement timeout
 COMMIT;
+-- Same as above, but for a streaming_fetch (cursor-free) scan: verify that
+-- canceling mid-stream correctly clears conn_state->active_scan, so the
+-- connection is safely reusable afterward.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+BEGIN;
+SELECT count(*) FROM ft1 a;
+ count 
+-------
+   822
+(1 row)
+
+SET LOCAL statement_timeout = '10ms';
+SELECT count(*) FROM ft1 a CROSS JOIN ft1 b CROSS JOIN ft1 c CROSS JOIN ft1 d;
+ERROR:  canceling statement due to statement timeout
+COMMIT;
+-- Must succeed cleanly if active_scan was properly cleared on cancel.
+SELECT count(*) FROM ft1;
+ count 
+-------
+   822
+(1 row)
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 5b539c4eeef..14e5c5cd32f 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -122,6 +122,7 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 			strcmp(def->defname, "parallel_abort") == 0 ||
 			strcmp(def->defname, "keep_connections") == 0 ||
 			strcmp(def->defname, "import_stats") == 0 ||
+			strcmp(def->defname, "streaming_fetch") == 0 ||
 			strcmp(def->defname, "use_scram_passthrough") == 0)
 		{
 			/* these accept only boolean values */
@@ -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 62dd4fe6346..853066fadd1 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -55,6 +55,7 @@
 #include "utils/sampling.h"
 #include "utils/selfuncs.h"
 #include "utils/timestamp.h"
+#include "utils/tuplestore.h"
 
 PG_MODULE_MAGIC_EXT(
 					.name = "postgres_fdw",
@@ -85,6 +86,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
@@ -206,6 +209,13 @@ typedef struct PgFdwScanState
 	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
 
 	int			fetch_size;		/* number of tuples per fetch */
+	/* Fields required for the streaming_fetch mode only */
+	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
+								 * drain_other_active_scan */
 } PgFdwScanState;
 
 /*
@@ -518,6 +528,7 @@ static void estimate_path_cost_size(PlannerInfo *root,
 									Cost *p_startup_cost, Cost *p_total_cost);
 static void get_remote_estimate(const char *sql,
 								PGconn *conn,
+								PgFdwConnState *conn_state,
 								double *rows,
 								int *width,
 								Cost *startup_cost,
@@ -533,6 +544,7 @@ static void adjust_foreign_grouping_path_cost(PlannerInfo *root,
 static bool ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
 									  EquivalenceClass *ec, EquivalenceMember *em,
 									  void *arg);
+static void prepare_query(ForeignScanState *node);
 static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number,
@@ -598,8 +610,8 @@ static bool fetch_remote_statistics(Relation relation,
 									RemoteStatsResults *remstats,
 									RemoteAttributeMapping **p_remattrmap,
 									int *p_attrcnt);
-static PGresult *fetch_relstats(PGconn *conn, Relation relation);
-static PGresult *fetch_attstats(PGconn *conn, int server_version_num,
+static PGresult *fetch_relstats(PGconn *conn, PgFdwConnState *conn_state, Relation relation);
+static PGresult *fetch_attstats(PGconn *conn, PgFdwConnState *conn_state, int server_version_num,
 								const char *remote_schemaname, const char *remote_relname,
 								const char *column_list);
 static RemoteAttributeMapping *build_remattrmap(Relation relation, List *va_cols,
@@ -667,6 +679,12 @@ 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 PGresult *fetch_stream_result(PgFdwScanState *fsstate);
+static void fetch_from_tuplestore(ForeignScanState *node);
+static void init_scan(ForeignScanState *node);
+static bool is_active_scan(PgFdwScanState *fsstate);
 
 /*
  * Foreign-data wrapper handler function: return a struct with pointers
@@ -776,9 +794,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,
@@ -1538,9 +1559,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));
 
 	/*
 	 * Position FdwScanPrivateRelations: either the EXPLAIN relation string
@@ -1767,6 +1788,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,
@@ -1819,6 +1842,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;
 }
 
 /*
@@ -1839,7 +1864,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.
@@ -1877,6 +1907,7 @@ postgresReScanForeignScan(ForeignScanState *node)
 	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
 	char		sql[64];
 	PGresult   *res;
+	bool		reinitialize_scan = false;
 
 	/* If no scan is in progress, nothing to do. */
 	if (!fsstate->scan_in_progress)
@@ -1900,40 +1931,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);
 			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) &&
+				!pgfdw_cancel_query(fsstate->conn, fsstate->conn_state))
+				ereport(ERROR,
+						errcode(ERRCODE_CONNECTION_FAILURE),
+						errmsg("could not cancel query"));
+			if (fsstate->tuplestore)
+			{
+				tuplestore_end(fsstate->tuplestore);
+				fsstate->tuplestore = NULL;
+			}
+		}
+		else
+		{
+			drain_other_active_scan(fsstate->conn_state);
+			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;
 }
 
 /*
@@ -1951,9 +2024,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) &&
+				!pgfdw_cancel_query(fsstate->conn, fsstate->conn_state))
+				ereport(ERROR,
+						errcode(ERRCODE_CONNECTION_FAILURE),
+						errmsg("could not cancel query"));
+			if (fsstate->tuplestore)
+				tuplestore_end(fsstate->tuplestore);
+
+			MemoryContextReset(fsstate->batch_cxt);
+		}
+		else
+		{
+			drain_other_active_scan(fsstate->conn_state);
+			close_cursor(fsstate->conn, fsstate->cursor_number,
+						 fsstate->conn_state);
+		}
+	}
 	/* Release remote connection */
 	ReleaseConnection(fsstate->conn);
 	fsstate->conn = NULL;
@@ -3233,9 +3324,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);
 	}
 }
 
@@ -3429,6 +3524,7 @@ estimate_path_cost_size(PlannerInfo *root,
 		List	   *local_param_join_conds;
 		StringInfoData sql;
 		PGconn	   *conn;
+		PgFdwConnState *conn_state;
 		Selectivity local_sel;
 		QualCost	local_cost;
 		List	   *fdw_scan_tlist = NIL;
@@ -3472,8 +3568,8 @@ estimate_path_cost_size(PlannerInfo *root,
 								false, &retrieved_attrs, NULL);
 
 		/* Get the remote estimate */
-		conn = GetConnection(fpinfo->user, false, NULL);
-		get_remote_estimate(sql.data, conn, &rows, &width,
+		conn = GetConnection(fpinfo->user, false, &conn_state);
+		get_remote_estimate(sql.data, conn, conn_state, &rows, &width,
 							&startup_cost, &total_cost);
 		ReleaseConnection(conn);
 
@@ -3919,7 +4015,7 @@ estimate_path_cost_size(PlannerInfo *root,
  * The given "sql" must be an EXPLAIN command.
  */
 static void
-get_remote_estimate(const char *sql, PGconn *conn,
+get_remote_estimate(const char *sql, PGconn *conn, PgFdwConnState *conn_state,
 					double *rows, int *width,
 					Cost *startup_cost, Cost *total_cost)
 {
@@ -3931,7 +4027,7 @@ get_remote_estimate(const char *sql, PGconn *conn,
 	/*
 	 * Execute EXPLAIN remotely.
 	 */
-	res = pgfdw_exec_query(conn, sql, NULL);
+	res = pgfdw_exec_query(conn, sql, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql);
 
@@ -4038,23 +4134,29 @@ ec_member_matches_foreign(PlannerInfo *root, RelOptInfo *rel,
 }
 
 /*
- * Create cursor for node's query with current parameter values.
+ * Do the work common to create_cursor() and init_scan(): finish any
+ * pending async request on this connection, marshal this scan's
+ * parameter values, and drain any other in-flight scan sharing the
+ * connection so it's safe to send a new command.
  */
 static void
-create_cursor(ForeignScanState *node)
+prepare_query(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;
-	StringInfoData buf;
-	PGresult   *res;
 
 	/* First, process a pending asynchronous request, if any. */
 	if (fsstate->conn_state->pendingAreq)
 		process_pending_request(fsstate->conn_state->pendingAreq);
 
+	/*
+	 * If the other scan is using streaming_fetch mode, then save the tuples
+	 * to tuplestore before starting a new scan.
+	 */
+	drain_other_active_scan(fsstate->conn_state);
+
 	/*
 	 * 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
@@ -4073,6 +4175,22 @@ create_cursor(ForeignScanState *node)
 
 		MemoryContextSwitchTo(oldcontext);
 	}
+}
+
+/*
+ * Create cursor for node's query with current parameter values.
+ */
+static void
+create_cursor(ForeignScanState *node)
+{
+	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+	PGconn	   *conn = fsstate->conn;
+	int			numParams = fsstate->numParams;
+	const char **values = fsstate->param_values;
+	StringInfoData buf;
+	PGresult   *res;
+
+	prepare_query(node);
 
 	/* Construct the DECLARE CURSOR command */
 	initStringInfo(&buf);
@@ -4110,6 +4228,44 @@ create_cursor(ForeignScanState *node)
 	pfree(buf.data);
 }
 
+/*
+ * Create a scan for the query, similar to create_cursor
+ * for streaming_fetch mode
+ */
+static void
+init_scan(ForeignScanState *node)
+{
+	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+	PGconn	   *conn = fsstate->conn;
+
+	prepare_query(node);
+
+	if (!PQsendQueryParams(conn, fsstate->query, fsstate->numParams,
+						   NULL, fsstate->param_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, fsstate->conn_state);
+		pgfdw_report_error(NULL, conn, fsstate->query);
+	}
+
+	/* Mark the scan as started, and show no tuples have been retrieved */
+	fsstate->scan_in_progress = true;
+	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;
+}
+
 /*
  * Fetch some more rows from the node's cursor.
  */
@@ -4123,14 +4279,18 @@ fetch_more_data(ForeignScanState *node)
 	int			i;
 	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);
 
+	/*
+	 * Count this as a fill of tuples[], regardless of which branch below ends
+	 * up supplying the data (or finds there is none), so that
+	 * postgresReScanForeignScan can rely on it to know how to rewind.
+	 */
+	if (fsstate->fetch_ct_2 < 2)
+		fsstate->fetch_ct_2++;
+
 	if (fsstate->async_capable)
 	{
 		Assert(fsstate->conn_state->pendingAreq);
@@ -4147,10 +4307,13 @@ fetch_more_data(ForeignScanState *node)
 		/* Reset per-connection state */
 		fsstate->conn_state->pendingAreq = NULL;
 	}
-	else
+	else if (!fsstate->streaming_fetch)
 	{
 		char		sql[64];
 
+		/* Drain the other scan before firing new scan. */
+		drain_other_active_scan(fsstate->conn_state);
+
 		/* This is a regular synchronous fetch. */
 		snprintf(sql, sizeof(sql), "FETCH %d FROM c%u",
 				 fsstate->fetch_size, fsstate->cursor_number);
@@ -4160,7 +4323,25 @@ fetch_more_data(ForeignScanState *node)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pgfdw_report_error(res, conn, fsstate->query);
 	}
-
+	else if (fsstate->tuplestore)
+	{
+		/*
+		 * When streaming_fetch is used, there is a special possibility --
+		 * reading from the tuplestore.
+		 */
+		fetch_from_tuplestore(node);
+		MemoryContextSwitchTo(oldcontext);
+		return;
+	}
+	else
+	{
+		res = fetch_stream_result(fsstate);
+		if (res == NULL)
+		{
+			MemoryContextSwitchTo(oldcontext);
+			return;
+		}
+	}
 	/* Convert the data into HeapTuples */
 	numrows = PQntuples(res);
 	fsstate->tuples = palloc0_array(HeapTuple, numrows);
@@ -4180,18 +4361,188 @@ fetch_more_data(ForeignScanState *node)
 									   fsstate->temp_cxt);
 	}
 
-	/* Update fetch_ct_2 */
-	if (fsstate->fetch_ct_2 < 2)
-		fsstate->fetch_ct_2++;
-
 	/* Must be EOF if we didn't get as many tuples as we asked for. */
 	fsstate->eof_reached = (numrows < fsstate->fetch_size);
 
 	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.  In the unexpected
+	 * event that more data actually follows, drain_other_active_scan() will
+	 * stash it rather than losing it.
+	 */
+	if (fsstate->streaming_fetch && fsstate->eof_reached)
+		drain_other_active_scan(fsstate->conn_state);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Get the next chunked-rows-mode result for a streaming_fetch scan.
+ * Returns the result to convert into tuples, or NULL if the scan is
+ * already fully handled (EOF was reached and active_scan cleared).
+ */
+static PGresult *
+fetch_stream_result(PgFdwScanState *fsstate)
+{
+	PGconn	   *conn = fsstate->conn;
+	PGresult   *res;
+
+	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 streaming_fetch mode.
+	 * Consume the trailing protocol NULL now and clear active_scan so a
+	 * concurrent init_scan does not call drain_other_active_scan 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;
+		return NULL;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_CHUNK)
+		pgfdw_report_error(res, conn, fsstate->query);
+
+	return res;
+}
+
+/*
+ * This is used in streaming_fetch mode only to fetch the tuples from the
+ * tuplestore.  At most fetch_size tuples are retrieved per call, matching
+ * the batch size used elsewhere for this scan; the tuplestore is released
+ * once it has been fully drained.
+ */
+static void
+fetch_from_tuplestore(ForeignScanState *node)
+{
+	PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state;
+	TupleTableSlot *slot;
+	int			numrows = 0;
+
+	/* Retrieve up to fetch_size tuples from the tuplestore at a time. */
+	fsstate->tuples = palloc0_array(HeapTuple, fsstate->fetch_size);
+	slot = MakeSingleTupleTableSlot(fsstate->tupdesc, &TTSOpsMinimalTuple);
+
+	while (numrows < fsstate->fetch_size &&
+		   tuplestore_gettupleslot(fsstate->tuplestore, true, true, slot))
+	{
+		fsstate->tuples[numrows++] = ExecFetchSlotHeapTuple(slot, true, NULL);
+		ExecClearTuple(slot);
+	}
+	fsstate->num_tuples = numrows;
+	fsstate->next_tuple = 0;
+
+	/* Must be EOF if we didn't get as many tuples as we asked for. */
+	fsstate->eof_reached = (numrows < fsstate->fetch_size);
+
+	ExecDropSingleTupleTableSlot(slot);
+
+	/* Clean up once the tuplestore has been fully drained. */
+	if (fsstate->eof_reached)
+	{
+		tuplestore_end(fsstate->tuplestore);
+		fsstate->tuplestore = NULL;
+	}
+}
+
+/*
+ * If some other scan on this connection is using streaming_fetch and still
+ * has an unconsumed result, drain it into a tuplestore so the connection
+ * becomes idle and can be reused for a new query.
+ *
+ * Not static: also called from GetConnection() and pgfdw_exec_query() in
+ * connection.c, for the same reason it's called throughout this file --
+ * anywhere a new query might be sent down a connection that could still
+ * have another streaming_fetch scan's result pending on it.
+ */
+void
+drain_other_active_scan(PgFdwConnState *conn_state)
+{
+	PgFdwScanState *active_fsstate = conn_state->active_scan;
+	MemoryContext oldcontext;
+
+	if (!active_fsstate)
+		return;
+
+	oldcontext = MemoryContextSwitchTo(MemoryContextGetParent(active_fsstate->batch_cxt));
+
+	/*
+	 * fetch_stream_result() hands back each PGRES_TUPLES_CHUNK result in turn
+	 * and, once the wire protocol is fully drained, clears active_scan and
+	 * returns NULL.  Keep calling it until then, saving every chunk we get
+	 * along the way.
+	 */
+	for (;;)
+	{
+		PGresult   *res;
+		int			numrows;
+		int			i;
+
+		CHECK_FOR_INTERRUPTS();
+
+		res = fetch_stream_result(active_fsstate);
+		if (res == NULL)
+			break;
+
+		if (active_fsstate->tuplestore == NULL)
+			active_fsstate->tuplestore = tuplestore_begin_heap(true, false, work_mem);
+
+		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);
+	}
+
+	/*
+	 * fetch_stream_result() unconditionally set eof_reached once it drained
+	 * the wire.  That's wrong if we saved any rows above: the scan still has
+	 * pending data to read from the tuplestore, so it must not be treated as
+	 * EOF yet.  fetch_from_tuplestore() will set eof_reached again once the
+	 * tuplestore itself is drained.
+	 */
+	if (active_fsstate->tuplestore != NULL)
+		active_fsstate->eof_reached = false;
+
 	MemoryContextSwitchTo(oldcontext);
 }
 
+/*
+ * Return true if fsstate is the connection's currently active
+ * streaming_fetch scan.
+ */
+static bool
+is_active_scan(PgFdwScanState *fsstate)
+{
+	return fsstate->streaming_fetch &&
+		fsstate->conn_state->active_scan == fsstate;
+}
+
 /*
  * Force assorted GUC parameters to settings that ensure that we'll output
  * data values in a form that is unambiguous to the remote server.
@@ -4416,6 +4767,12 @@ execute_foreign_modify(EState *estate,
 	if (fmstate->conn_state->pendingAreq)
 		process_pending_request(fmstate->conn_state->pendingAreq);
 
+	/*
+	 * 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);
+
 	/*
 	 * If the existing query was deparsed and prepared for a different number
 	 * of rows, rebuild it for the proper number.
@@ -4842,6 +5199,12 @@ execute_dml_stmt(ForeignScanState *node)
 	if (dmstate->conn_state->pendingAreq)
 		process_pending_request(dmstate->conn_state->pendingAreq);
 
+	/*
+	 * 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);
+
 	/*
 	 * Construct array of query parameter values in text format.
 	 */
@@ -5212,6 +5575,7 @@ postgresAnalyzeForeignTable(Relation relation,
 	ForeignTable *table;
 	UserMapping *user;
 	PGconn	   *conn;
+	PgFdwConnState *conn_state;
 	StringInfoData sql;
 	PGresult   *res;
 
@@ -5231,7 +5595,7 @@ postgresAnalyzeForeignTable(Relation relation,
 	 */
 	table = GetForeignTable(RelationGetRelid(relation));
 	user = GetUserMapping(relation->rd_rel->relowner, table->serverid);
-	conn = GetConnection(user, false, NULL);
+	conn = GetConnection(user, false, &conn_state);
 
 	/*
 	 * Construct command to get page count for relation.
@@ -5239,7 +5603,7 @@ postgresAnalyzeForeignTable(Relation relation,
 	initStringInfo(&sql);
 	deparseAnalyzeSizeSql(&sql, relation);
 
-	res = pgfdw_exec_query(conn, sql.data, NULL);
+	res = pgfdw_exec_query(conn, sql.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
@@ -5266,6 +5630,7 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample)
 	ForeignTable *table;
 	UserMapping *user;
 	PGconn	   *conn;
+	PgFdwConnState *conn_state;
 	StringInfoData sql;
 	PGresult   *res;
 	double		reltuples;
@@ -5280,7 +5645,7 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample)
 	 */
 	table = GetForeignTable(RelationGetRelid(relation));
 	user = GetUserMapping(relation->rd_rel->relowner, table->serverid);
-	conn = GetConnection(user, false, NULL);
+	conn = GetConnection(user, false, &conn_state);
 
 	/*
 	 * Construct command to get page count for relation.
@@ -5288,7 +5653,7 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample)
 	initStringInfo(&sql);
 	deparseAnalyzeInfoSql(&sql, relation);
 
-	res = pgfdw_exec_query(conn, sql.data, NULL);
+	res = pgfdw_exec_query(conn, sql.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
@@ -5334,6 +5699,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 	ForeignServer *server;
 	UserMapping *user;
 	PGconn	   *conn;
+	PgFdwConnState *conn_state;
 	int			server_version_num;
 	PgFdwSamplingMethod method = ANALYZE_SAMPLE_AUTO;	/* auto is default */
 	double		sample_frac = -1.0;
@@ -5369,7 +5735,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 	table = GetForeignTable(RelationGetRelid(relation));
 	server = GetForeignServer(table->serverid);
 	user = GetUserMapping(relation->rd_rel->relowner, table->serverid);
-	conn = GetConnection(user, false, NULL);
+	conn = GetConnection(user, false, &conn_state);
 
 	/* We'll need server version, so fetch it now. */
 	server_version_num = PQserverVersion(conn);
@@ -5519,7 +5885,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 
 	deparseAnalyzeSql(&sql, relation, method, sample_frac, &astate.retrieved_attrs);
 
-	res = pgfdw_exec_query(conn, sql.data, NULL);
+	res = pgfdw_exec_query(conn, sql.data, conn_state);
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		pgfdw_report_error(res, conn, sql.data);
 	PQclear(res);
@@ -5570,7 +5936,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 		 */
 
 		/* Fetch some rows */
-		res = pgfdw_exec_query(conn, fetch_sql, NULL);
+		res = pgfdw_exec_query(conn, fetch_sql, conn_state);
 		/* On error, report the original query, not the FETCH. */
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pgfdw_report_error(res, conn, sql.data);
@@ -5588,7 +5954,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 	}
 
 	/* Close the cursor, just to be tidy. */
-	close_cursor(conn, cursor_number, NULL);
+	close_cursor(conn, cursor_number, conn_state);
 
 	ReleaseConnection(conn);
 
@@ -5804,6 +6170,7 @@ fetch_remote_statistics(Relation relation,
 	const char *remote_relname = NULL;
 	UserMapping *user;
 	PGconn	   *conn;
+	PgFdwConnState *conn_state;
 	PGresult   *relstats = NULL;
 	PGresult   *attstats = NULL;
 	int			server_version_num;
@@ -5836,11 +6203,11 @@ fetch_remote_statistics(Relation relation,
 	 * pg_stats, so we do the remote access as the current user.
 	 */
 	user = GetUserMapping(GetUserId(), table->serverid);
-	conn = GetConnection(user, false, NULL);
+	conn = GetConnection(user, false, &conn_state);
 	remstats->version = server_version_num = PQserverVersion(conn);
 
 	/* Fetch relation stats. */
-	remstats->rel = relstats = fetch_relstats(conn, relation);
+	remstats->rel = relstats = fetch_relstats(conn, conn_state, relation);
 
 	/*
 	 * Verify that the remote table is the sort that can have meaningful stats
@@ -5895,7 +6262,7 @@ fetch_remote_statistics(Relation relation,
 		if (attrcnt > 0)
 		{
 			/* Fetch attribute stats. */
-			remstats->att = attstats = fetch_attstats(conn,
+			remstats->att = attstats = fetch_attstats(conn, conn_state,
 													  server_version_num,
 													  remote_schemaname,
 													  remote_relname,
@@ -5934,7 +6301,7 @@ fetch_cleanup:
  * Attempt to fetch remote relation stats.
  */
 static PGresult *
-fetch_relstats(PGconn *conn, Relation relation)
+fetch_relstats(PGconn *conn, PgFdwConnState *conn_state, Relation relation)
 {
 	StringInfoData sql;
 	PGresult   *res;
@@ -5942,7 +6309,7 @@ fetch_relstats(PGconn *conn, Relation relation)
 	initStringInfo(&sql);
 	deparseAnalyzeInfoSql(&sql, relation);
 
-	res = pgfdw_exec_query(conn, sql.data, NULL);
+	res = pgfdw_exec_query(conn, sql.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
@@ -5956,7 +6323,7 @@ fetch_relstats(PGconn *conn, Relation relation)
  * Attempt to fetch remote attribute stats.
  */
 static PGresult *
-fetch_attstats(PGconn *conn, int server_version_num,
+fetch_attstats(PGconn *conn, PgFdwConnState *conn_state, int server_version_num,
 			   const char *remote_schemaname, const char *remote_relname,
 			   const char *column_list)
 {
@@ -6013,7 +6380,7 @@ fetch_attstats(PGconn *conn, int server_version_num,
 		appendStringInfoString(&sql,
 							   " ORDER BY attname COLLATE \"C\"");
 
-	res = pgfdw_exec_query(conn, sql.data, NULL);
+	res = pgfdw_exec_query(conn, sql.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
@@ -6468,6 +6835,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 	ForeignServer *server;
 	UserMapping *mapping;
 	PGconn	   *conn;
+	PgFdwConnState *conn_state;
 	StringInfoData buf;
 	PGresult   *res;
 	int			numrows,
@@ -6499,7 +6867,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 	 */
 	server = GetForeignServer(serverOid);
 	mapping = GetUserMapping(GetUserId(), server->serverid);
-	conn = GetConnection(mapping, false, NULL);
+	conn = GetConnection(mapping, false, &conn_state);
 
 	/* Don't attempt to import collation if remote server hasn't got it */
 	if (PQserverVersion(conn) < 90100)
@@ -6512,7 +6880,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 	appendStringInfoString(&buf, "SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = ");
 	deparseStringLiteral(&buf, stmt->remote_schema);
 
-	res = pgfdw_exec_query(conn, buf.data, NULL);
+	res = pgfdw_exec_query(conn, buf.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, buf.data);
 
@@ -6626,7 +6994,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 	appendStringInfoString(&buf, " ORDER BY c.relname, a.attnum");
 
 	/* Fetch the data */
-	res = pgfdw_exec_query(conn, buf.data, NULL);
+	res = pgfdw_exec_query(conn, buf.data, conn_state);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, buf.data);
 
@@ -7557,6 +7925,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);
 	}
 }
 
@@ -7580,9 +7950,17 @@ 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);
+}
+
 /*
  * Merge FDW options from input relations into a new set of options for a join
  * or an upper rel.
@@ -7615,6 +7993,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)
@@ -7646,6 +8025,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;
 	}
 }
 
@@ -8800,6 +9184,12 @@ fetch_more_data_begin(AsyncRequest *areq)
 	if (!fsstate->scan_in_progress)
 		create_cursor(node);
 
+	/*
+	 * If the other scan is using streaming_fetch mode, then save the tuples
+	 * to tuplestore before sending a new query down this connection.
+	 */
+	drain_other_active_scan(fsstate->conn_state);
+
 	/* We will send this query, but not wait for the response. */
 	snprintf(sql, sizeof(sql), "FETCH %d FROM c%u",
 			 fsstate->fetch_size, fsstate->cursor_number);
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index da7da1c2ea9..092ea580e21 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
@@ -141,12 +142,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;	/* the streaming_fetch scan, if any, that
+									 * currently has an unconsumed result */
 } PgFdwConnState;
 
 /*
@@ -165,6 +170,7 @@ typedef enum PgFdwSamplingMethod
 extern int	set_transmission_modes(void);
 extern void reset_transmission_modes(int nestlevel);
 extern void process_pending_request(AsyncRequest *areq);
+extern void drain_other_active_scan(PgFdwConnState *conn_state);
 
 /* in connection.c */
 extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt,
@@ -174,8 +180,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, PgFdwConnState *state);
 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 ec766e2b28a..313bd3f0deb 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -267,6 +267,153 @@ 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');
+
+CREATE FOREIGN TABLE ft_server (id int, val text)
+    SERVER fetch_stream_srv
+    OPTIONS (schema_name 'public', table_name 'local_tbl');
+
+-- Combined view of the option at both levels, used by the checks below.
+CREATE VIEW streaming_fetch_opt AS
+SELECT (SELECT option_value FROM pg_foreign_server,
+               LATERAL pg_options_to_table(srvoptions)
+        WHERE srvname = 'fetch_stream_srv'
+          AND option_name = 'streaming_fetch') AS server_val,
+       (SELECT ftoptions FROM pg_foreign_table
+        WHERE ftrelid = 'ft_server'::regclass) AS table_opts;
+
+-- 1. streaming_fetch set at SERVER level only: value tracks the server,
+-- and (being unset) never shows up in the table's own ftoptions.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;
+
+-- 2. streaming_fetch set at TABLE level only (no server-level option);
+-- query results must be correct for both true and false.
+ALTER SERVER fetch_stream_srv OPTIONS (DROP streaming_fetch);
+ALTER FOREIGN TABLE ft_server OPTIONS (ADD streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;
+SELECT * FROM ft_server ORDER BY id;
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;
+SELECT * FROM ft_server ORDER BY id;
+
+-- 3. TABLE-level value overrides SERVER-level value; query must use the
+-- effective (table-level) value in both directions.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD streaming_fetch 'true');
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'false');
+SELECT * FROM streaming_fetch_opt;  -- server=true, table overrides to false
+SELECT * FROM ft_server ORDER BY id;
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch 'false');
+ALTER FOREIGN TABLE ft_server OPTIONS (SET streaming_fetch 'true');
+SELECT * FROM streaming_fetch_opt;  -- server=false, table overrides to true
+SELECT * FROM ft_server ORDER BY id;
+
+DROP VIEW streaming_fetch_opt;
+
+-- 4. Negative tests: invalid values must be rejected, at both table and
+-- server level.
+\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
+
+ALTER SERVER fetch_stream_srv OPTIONS (SET streaming_fetch '1');       -- ERROR
+ALTER SERVER fetch_stream_srv OPTIONS (SET 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 ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (ADD streaming_fetch 'false');
+SELECT ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (SET streaming_fetch 'true');
+SELECT ftoptions FROM pg_foreign_table WHERE ftrelid = 'ft_alter_test'::regclass;
+
+-- DROP table-level option (falls back to server-level), and confirm the
+-- fallback value is functionally correct.
+ALTER FOREIGN TABLE ft_alter_test OPTIONS (DROP streaming_fetch);
+SELECT 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;
+
+-- 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;
+
+-- fetch_size = 5: three chunks with a partial last chunk (5+5+2);
+-- the final chunk is smaller than fetch_size.
+ALTER FOREIGN TABLE ft_fetchsize OPTIONS (SET fetch_size '5');
+SELECT count(*) FROM ft_fetchsize;
+SELECT * FROM ft_fetchsize ORDER BY id;
+
+-- fetch_size exceeds the table row count: all rows arrive in one chunk
+-- followed immediately by the final empty PGRES_TUPLES_OK result.
+ALTER FOREIGN TABLE ft_fetchsize OPTIONS (SET 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.
+-- ft_server's table-level option is still 'true' from step 3 above.
+ALTER SERVER fetch_stream_srv OPTIONS (ADD use_remote_estimate 'true');
+
+-- Verify both options are active.
+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.
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft_server ORDER BY id;
+SELECT * FROM ft_server ORDER BY id;
+
+ALTER SERVER fetch_stream_srv OPTIONS (DROP use_remote_estimate);
+
+-- Cleanup
+DROP FOREIGN TABLE ft_server;
+DROP USER MAPPING FOR CURRENT_USER SERVER fetch_stream_srv;
+DROP SERVER fetch_stream_srv CASCADE;
+DROP TABLE local_tbl;
 -- ===================================================================
 -- test subscription
 -- ===================================================================
@@ -307,6 +454,33 @@ 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 +560,208 @@ 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);
+
+-- Test in streaming_fetch mode when a scan that is still the
+-- connection's active_scan (i.e. it has not reached EOF) correctly clears
+-- active_scan.
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+SET statement_timeout = '20s';
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+
+RESET statement_timeout;
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (SET fetch_size '100');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+-- Verify that aborting a transaction while a streaming_fetch scan still has
+-- an unconsumed chunked result correctly resets active_scan, so the
+-- connection can be reused afterward.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1');
+
+BEGIN;
+-- Fetch only the first row, leaving the stream unconsumed.
+SELECT c1 FROM ft1 ORDER BY c1 LIMIT 1;
+SELECT 1/0;  -- force an error, aborting with the scan still active
+ROLLBACK;
+
+SELECT count(*) FROM ft1;  -- must succeed
+
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
+
+-- Test that fetch_more_data()'s plain cursor-mode FETCH on ft1's own
+-- already-open cursor correctly accounts for another scan's active_scan.
+-- ft1's fetch_size is set below its matching row count, so its cursor
+-- needs a second FETCH; each outer row's EXISTS subplan on ft2 leaves the
+-- inner streaming_fetch scan as the connection's active_scan without
+-- reaching EOF (fetch_size '1' on ft2, and EXISTS only needs one match),
+-- so that second FETCH must drain it first (pgfdw_exec_query() does this
+-- automatically) or ft2's unconsumed chunked result would desync the
+-- connection's result stream.
+ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '5');
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+
+SELECT a.c1, a.c2 FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+-- Test that fetch_from_tuplestore() correctly drains a large backlog in
+-- fetch_size-sized batches rather than all at once: with a small
+-- fetch_size and a self-join matching many rows per outer row, the outer
+-- scan's tuplestore (populated when the inner scan interrupts it to reuse
+-- the connection) needs several batched reads to fully drain.  If a batch
+-- were dropped or duplicated, the count below would not match.
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '5');
+SELECT count(*) 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 (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+-- Test that execute_foreign_modify() correctly drains another scan's
+-- undrained streaming_fetch result before sending its own prepared
+-- statement.  The EXISTS subplan against ft2 (streaming_fetch, fetch_size
+-- 1) leaves ft2 as the connection's active_scan without reaching EOF after
+-- each outer row (short-circuiting on the first match), so each matching
+-- row's UPDATE on ft1 -- sent as a separate prepared statement per row,
+-- since the EXISTS subplan prevents direct modify -- must drain it first.
+-- The returned c1 values must match the known-correct row set for this
+-- same EXISTS predicate (see the plain SELECT variant of this query above).
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE ft1 SET c3 = c3
+WHERE c1 < 20 AND c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = ft1.c2);
+
+BEGIN;
+UPDATE ft1 SET c3 = c3
+WHERE c1 < 20 AND c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = ft1.c2)
+RETURNING c1;
+ROLLBACK;
+
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+
+-- Test that execute_dml_stmt() (a fully pushed-down direct-modify
+-- UPDATE/DELETE) also drains another scan's undrained streaming_fetch
+-- result before sending its own query down the shared connection.  ft4
+-- shares the loopback connection with ft2; direct_modify_ft4() performs a
+-- self-contained, direct-modify-eligible UPDATE on ft4, called once per
+-- matching ft1 row while ft2's streaming EXISTS subplan is mid-stream.
+CREATE FUNCTION direct_modify_ft4(id int) RETURNS void AS $$
+  UPDATE ft4 SET c3 = c3 WHERE c1 = id;
+$$ LANGUAGE sql;
+
+EXPLAIN (VERBOSE, COSTS OFF) UPDATE ft4 SET c3 = c3 WHERE c1 = 1;
+
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'true');
+ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1');
+SET enable_hashagg = off;
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_material = off;
+
+BEGIN;
+SELECT direct_modify_ft4(a.c1) FROM ft1 a
+WHERE a.c1 < 20 AND a.c8 = 'foo' AND EXISTS (SELECT 1 FROM ft2 b WHERE b.c2 = a.c2)
+ORDER BY a.c1;
+ROLLBACK;
+
+RESET enable_hashagg;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_material;
+ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size);
+ALTER FOREIGN TABLE ft2 OPTIONS (SET streaming_fetch 'false');
+DROP FUNCTION direct_modify_ft4(int);
+
+-- Test in streaming_fetch mode for interleaved scans.
+-- The non-shippable condition a.c8 = 'foo' prevents full
+-- join pushdown, so the planner issues two separate FDW scans.
+-- When the inner scan is initiated it first drains 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);
+
+
 -- 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));
@@ -711,6 +1087,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;
@@ -719,6 +1105,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)
@@ -1140,6 +1532,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;
@@ -3909,6 +4326,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
 -- =============================================================================
@@ -4477,6 +4929,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/contrib/postgres_fdw/sql/query_cancel.sql b/contrib/postgres_fdw/sql/query_cancel.sql
index a6830705843..d4d15ccaf8f 100644
--- a/contrib/postgres_fdw/sql/query_cancel.sql
+++ b/contrib/postgres_fdw/sql/query_cancel.sql
@@ -20,3 +20,19 @@ SET LOCAL statement_timeout = '10ms';
 -- This would take very long if not canceled:
 SELECT count(*) FROM ft1 a CROSS JOIN ft1 b CROSS JOIN ft1 c CROSS JOIN ft1 d;
 COMMIT;
+
+-- Same as above, but for a streaming_fetch (cursor-free) scan: verify that
+-- canceling mid-stream correctly clears conn_state->active_scan, so the
+-- connection is safely reusable afterward.
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'true');
+
+BEGIN;
+SELECT count(*) FROM ft1 a;
+SET LOCAL statement_timeout = '10ms';
+SELECT count(*) FROM ft1 a CROSS JOIN ft1 b CROSS JOIN ft1 c CROSS JOIN ft1 d;
+COMMIT;
+
+-- Must succeed cleanly if active_scan was properly cleared on cancel.
+SELECT count(*) FROM ft1;
+
+ALTER FOREIGN TABLE ft1 OPTIONS (SET streaming_fetch 'false');
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 87b1433aacb..e4ed483b0a3 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -476,6 +476,23 @@ 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>. It can be
+       specified for a foreign table or a foreign server. The option
+       specified on a table overrides an option specified for the server.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
 
   </sect3>
-- 
2.39.5 (Apple Git-154)

