diff -ru a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c --- a/contrib/postgres_fdw/connection.c 2026-10-03 05:55:27 +++ b/contrib/postgres_fdw/connection.c 2026-10-03 05:55:27 @@ -1091,6 +1091,16 @@ } /* + * Return the nesting depth of the remote (sub)transaction currently open on + * the connection (0 if none). + */ +int +pgfdw_remote_xact_depth(ConnCacheEntry *entry) +{ + return entry->xact_depth; +} + +/* * Submit a query and wait for the result. * * Since we don't use non-blocking mode, this can't process interrupts while diff -ru a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out --- a/contrib/postgres_fdw/expected/postgres_fdw.out 2026-10-03 05:55:27 +++ b/contrib/postgres_fdw/expected/postgres_fdw.out 2026-10-03 05:55:27 @@ -5373,9 +5373,50 @@ (1 row) COMMIT; +-- first fetch within a savepoint is fine if remote savepoint level is +-- not advanced beyond the one the cursor was created in BEGIN; DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; SAVEPOINT s; +FETCH c; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 +----+----+-------+------------------------------+--------------------------+----+------------+----- + 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo +(1 row) + +ROLLBACK TO s; +FETCH c; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 +----+----+-------+------------------------------+--------------------------+----+------------+----- + 2 | 2 | 00002 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo +(1 row) + +COMMIT; +-- ... but not otherwise +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT count(*) FROM ft1; + count +------- + 1000 +(1 row) + +FETCH c; +ERROR: cannot perform the first fetch of a cursor within a deeper subtransaction than it was created in +ABORT; +-- a cursor created in a released savepoint is handed to its parent +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE s1; +SAVEPOINT s2; +SELECT count(*) FROM ft1; + count +------- + 1000 +(1 row) + FETCH c; ERROR: cannot perform the first fetch of a cursor within a deeper subtransaction than it was created in ABORT; diff -ru a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c --- a/contrib/postgres_fdw/postgres_fdw.c 2026-10-03 05:55:27 +++ b/contrib/postgres_fdw/postgres_fdw.c 2026-10-03 05:55:27 @@ -190,7 +190,8 @@ FmgrInfo *param_flinfo; /* output conversion functions for them */ List *param_exprs; /* executable expressions for param values */ const char **param_values; /* textual values of query parameters */ - int created_at; /* xact depth at which the scan was created */ + SubTransactionId created_subid; /* subxact in which the scan was created */ + int created_level; /* its nesting depth at that time */ /* for storing result tuples */ HeapTuple *tuples; /* array of currently-retrieved tuples */ @@ -1765,8 +1766,9 @@ fsstate->cursor_number = GetCursorNumber(fsstate->conn); fsstate->cursor_exists = false; - /* Get the current local transaction's nesting depth */ - fsstate->created_at = GetCurrentTransactionNestLevel(); + /* Remember the local (sub)transaction that the scan is created in */ + fsstate->created_subid = GetCurrentSubTransactionId(); + fsstate->created_level = GetCurrentTransactionNestLevel(); /* Get private info created by planner functions. */ fsstate->query = strVal(list_nth(fsplan->fdw_private, @@ -4059,22 +4061,34 @@ StringInfoData buf; PGresult *res; - if (fsstate->created_at < GetCurrentTransactionNestLevel()) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot perform the first fetch of a cursor within a deeper subtransaction than it was created in"))); + /* + * The remote cursor is declared at the current remote (sub)transaction + * depth, and rolling back a remote savepoint at or below that depth + * destroys it. That is only safe if every local savepoint whose rollback + * keeps the local cursor alive is deeper than that. The local cursor + * lives at the depth of the (sub)transaction that created it, if that is + * still open; if it has been released, the cursor has been handed to some + * enclosing level, which we conservatively assume is the top level. + */ + { + int cursor_level; + if (SubTransactionIsActive(fsstate->created_subid)) + cursor_level = fsstate->created_level; + else + cursor_level = 1; + + if (pgfdw_remote_xact_depth(fsstate->conn_state->entry) > cursor_level) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot perform the first fetch of a cursor within a deeper subtransaction than it was created in"))); + } + /* First, process a pending asynchronous request, if any. */ if (fsstate->conn_state->pendingAreq) process_pending_request(fsstate->conn_state->pendingAreq); /* - * Second, synchronize the local/remote transactions. Note that we need - * to do this because this function can be called from open cursors. - */ - pgfdw_begin_remote_xact(fsstate->conn_state->entry); - - /* * Construct array of query parameter values in text format. We do the * conversions in the short-lived per-tuple context, so as not to cause a * memory leak over repeated scans. @@ -4116,6 +4130,15 @@ if (PQresultStatus(res) != PGRES_COMMAND_OK) pgfdw_report_error(res, conn, fsstate->query); PQclear(res); + + /* + * Now synchronize the local/remote transactions. We do this after + * declaring the cursor, not before, so that the remote cursor is not + * created inside a remote savepoint that the local cursor doesn't + * belong to. (We need to synchronize here because this function can be + * called from open cursors.) + */ + pgfdw_begin_remote_xact(fsstate->conn_state->entry); /* Mark the cursor as created, and show no tuples have been retrieved */ fsstate->cursor_exists = true; diff -ru a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h --- a/contrib/postgres_fdw/postgres_fdw.h 2026-10-03 05:55:27 +++ b/contrib/postgres_fdw/postgres_fdw.h 2026-10-03 05:55:27 @@ -175,6 +175,7 @@ extern unsigned int GetPrepStmtNumber(PGconn *conn); extern void do_sql_command(PGconn *conn, const char *sql); extern void pgfdw_begin_remote_xact(struct ConnCacheEntry *entry); +extern int pgfdw_remote_xact_depth(struct ConnCacheEntry *entry); extern PGresult *pgfdw_get_result(PGconn *conn); extern PGresult *pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state); diff -ru a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql --- a/contrib/postgres_fdw/sql/postgres_fdw.sql 2026-10-03 05:55:27 +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql 2026-10-03 05:55:27 @@ -1652,9 +1652,31 @@ SELECT * FROM ft1 ORDER BY c1 LIMIT 1; COMMIT; +-- first fetch within a savepoint is fine if remote savepoint level is +-- not advanced beyond the one the cursor was created in BEGIN; DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +FETCH c; +COMMIT; + +-- ... but not otherwise +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT count(*) FROM ft1; +FETCH c; +ABORT; + +-- a cursor created in a released savepoint is handed to its parent +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE s1; +SAVEPOINT s2; +SELECT count(*) FROM ft1; FETCH c; ABORT;