From f869b19b9f451f8e56dc0e19e1773b1b2db59512 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Fri, 16 Oct 2026 11:04:15 -0300 Subject: [PATCH v4] postgres_fdw: Fix handling of open cursors across subtransaction rollback. postgres_fdw declares the remote cursor for a foreign scan at the first FETCH, inside the remote savepoint matching the local subtransaction that is current at that time. If that subtransaction was rolled back while the local cursor was still alive (e.g., a cursor opened before a savepoint and first fetched after it), the remote cursor was destroyed together with its remote savepoint, but the local scan kept using it. This resulted in an error such as 'cursor "c1" does not exist', which was reported at some later point, e.g., when the cursor was closed at commit. Fix by keeping track of the foreign scans that are open in the backend, and of the nesting level at which each remote cursor was declared, so that the subtransaction callbacks can find and update them: on subtransaction commit, hand the cursors declared in it over to the parent level, matching the remote RELEASE; on abort, mark them as lost. The rows a scan has already fetched remain usable after its cursor is lost. Fetching more rows from the remote server fails with a clear error instead. A rescan of such a scan either reuses the rows in memory or creates a new cursor, and EndForeignScan doesn't try to close a cursor that is already gone. The remote (sub)transaction is now also synchronized with the local one before a cursor is created and before each FETCH, MOVE and CLOSE, so that a cursor opened before a transaction mode change (e.g., SET TRANSACTION READ ONLY) picks up the new mode. Since the registry holds pointers into executor memory, make sure an entry never outlives its scan: besides removing it in EndForeignScan, a memory context callback on the scan's executor memory removes it too. This matters because the executor doesn't call EndForeignScan for a scan whose portal was marked failed, which can happen when an error occurs while an outer-level cursor is being fetched inside a savepoint. Also, don't assume that a scan has a single relation when reporting that no more rows can be fetched, since a scan for a pushed-down join or aggregate has none. --- contrib/postgres_fdw/connection.c | 70 +++- .../postgres_fdw/expected/postgres_fdw.out | 353 ++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 288 +++++++++++++- contrib/postgres_fdw/postgres_fdw.h | 4 + contrib/postgres_fdw/sql/postgres_fdw.sql | 223 +++++++++++ src/tools/pgindent/typedefs.list | 1 + 6 files changed, 926 insertions(+), 13 deletions(-) diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 17f8c1e8f31..ef81c636baf 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -411,7 +411,8 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user) entry->mapping_hashvalue = GetSysCacheHashValue1(USERMAPPINGOID, ObjectIdGetDatum(user->umid)); - memset(&entry->state, 0, sizeof(entry->state)); + entry->state.pendingAreq = NULL; + entry->state.entry = entry; /* * Determine whether to keep the connection that we're about to make here @@ -1075,6 +1076,20 @@ GetPrepStmtNumber(PGconn *conn) return ++prep_stmt_number; } +/* + * Exported version of begin_remote_xact(). + * + * This can be called for connections on which begin_remote_xact() has started + * a remote transaction. + */ +void +pgfdw_begin_remote_xact(ConnCacheEntry *entry) +{ + Assert(entry); + Assert(entry->xact_depth > 0); + begin_remote_xact(entry); +} + /* * Submit a query and wait for the result. * @@ -1091,6 +1106,13 @@ pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state) if (state && state->pendingAreq) process_pending_request(state->pendingAreq); + /* + * Second, synchronize the local/remote transactions. Note that we need + * to do this because this function can be called from open cursors. + */ + if (state) + pgfdw_begin_remote_xact(state->entry); + if (!PQsendQuery(conn, query)) return NULL; return pgfdw_get_result(conn); @@ -1194,6 +1216,16 @@ pgfdw_xact_callback(XactEvent event, void *arg) List *pending_entries = NIL; List *cancel_requested = NIL; + /* + * If we are called for post-commit cleanup, all we need to do is clean up + * foreign scans. + */ + if (event == XACT_EVENT_PARALLEL_COMMIT || event == XACT_EVENT_COMMIT) + { + AtEOXact_ForeignScan(); + return; + } + /* Quick exit if no connections were touched in this transaction. */ if (!xact_got_connection) return; @@ -1357,6 +1389,10 @@ pgfdw_xact_callback(XactEvent event, void *arg) /* Likewise for read_only_level */ read_only_level = 0; + + /* If we are called for abort cleanup, clean up foreign scans as well */ + if (event == XACT_EVENT_PARALLEL_ABORT || event == XACT_EVENT_ABORT) + AtEOXact_ForeignScan(); } /* @@ -1372,8 +1408,9 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, List *pending_entries = NIL; List *cancel_requested = NIL; - /* Nothing to do at subxact start, nor after commit. */ + /* Nothing to do at subxact start. */ if (!(event == SUBXACT_EVENT_PRE_COMMIT_SUB || + event == SUBXACT_EVENT_COMMIT_SUB || event == SUBXACT_EVENT_ABORT_SUB)) return; @@ -1381,11 +1418,22 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, if (!xact_got_connection) return; + curlevel = GetCurrentTransactionNestLevel(); + + /* + * If we are called for post-subcommit cleanup, all we need to do is clean + * up foreign scans. + */ + if (event == SUBXACT_EVENT_COMMIT_SUB) + { + AtEOSubXact_ForeignScan(curlevel, true); + return; + } + /* * Scan all connection cache entries to find open remote subtransactions * of the current level, and close them. */ - curlevel = GetCurrentTransactionNestLevel(); hash_seq_init(&scan, ConnectionHash); while ((entry = (ConnCacheEntry *) hash_seq_search(&scan))) { @@ -1459,6 +1507,10 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, /* If in read_only_level, reset it */ if (curlevel == read_only_level) read_only_level = 0; + + /* If we are called for subabort cleanup, clean up foreign scans as well */ + if (event == SUBXACT_EVENT_ABORT_SUB) + AtEOSubXact_ForeignScan(curlevel, false); } /* @@ -1966,10 +2018,10 @@ 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(); in that case reset pendingAreq here. */ if (entry->state.pendingAreq) - memset(&entry->state, 0, sizeof(entry->state)); + entry->state.pendingAreq = NULL; /* Disarm changing_xact_state if it all worked */ entry->changing_xact_state = false; @@ -2263,9 +2315,9 @@ pgfdw_finish_abort_cleanup(List *pending_entries, List *cancel_requested, entry->have_error = false; } - /* Reset the per-connection state if needed */ + /* Reset pendingAreq here if any */ if (entry->state.pendingAreq) - memset(&entry->state, 0, sizeof(entry->state)); + entry->state.pendingAreq = NULL; /* We're done with this entry; unset the changing_xact_state flag */ entry->changing_xact_state = false; @@ -2308,9 +2360,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 */ + /* Reset pendingAreq here if any */ if (entry->state.pendingAreq) - memset(&entry->state, 0, sizeof(entry->state)); + entry->state.pendingAreq = 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 115ebc7925f..fe2bb8695d8 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -5358,6 +5358,9 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE user_enum; -- =================================================================== -- subtransaction -- + local/remote error doesn't break cursor +-- + we allow cursors opened before a savepoint to be first fetched +-- after the savepoint; if that subtransaction aborts, the rows already +-- fetched can still be returned, but no more can be fetched. -- =================================================================== BEGIN; DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; @@ -5396,6 +5399,356 @@ SELECT * FROM ft1 ORDER BY c1 LIMIT 1; 1 | 1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo (1 row) +COMMIT; +-- the rows fetched in the first batch are still available +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; +RELEASE SAVEPOINT 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) + +CLOSE c; +COMMIT; +-- but fetching another batch is not possible +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +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; +RELEASE SAVEPOINT s; +FETCH c; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +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; +RELEASE SAVEPOINT s; +CLOSE c; +COMMIT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); +-- a rescan of a scan whose cursor was lost creates a new one +SET enable_hashjoin = off; +SET enable_mergejoin = off; +SET enable_material = off; +EXPLAIN (COSTS OFF) +SELECT * FROM (VALUES (1), (2), (3)) v(x), + LATERAL (SELECT c1 FROM ft1 WHERE c1 = v.x OFFSET 0) s; + QUERY PLAN +--------------------------------- + Nested Loop + -> Values Scan on "*VALUES*" + -> Foreign Scan on ft1 +(3 rows) + +BEGIN; +DECLARE c CURSOR FOR +SELECT * FROM (VALUES (1), (2), (3)) v(x), + LATERAL (SELECT c1 FROM ft1 WHERE c1 = v.x OFFSET 0) s; +SAVEPOINT s; +FETCH c; + x | c1 +---+---- + 1 | 1 +(1 row) + +ROLLBACK TO s; +FETCH ALL c; + x | c1 +---+---- + 2 | 2 + 3 | 3 +(2 rows) + +COMMIT; +RESET enable_hashjoin; +RESET enable_mergejoin; +RESET enable_material; +-- cursor created in a subtransaction is handed to its parent subtransaction +BEGIN; +SAVEPOINT s; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE 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) + +SAVEPOINT 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) + +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 +----+----+-------+------------------------------+--------------------------+----+------------+----- + 3 | 3 | 00003 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo +(1 row) + +CLOSE c; +COMMIT; +-- a cursor created in a subtransaction, which is released, can be first +-- fetched in a sibling subtransaction that is then rolled back +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE SAVEPOINT s1; +SAVEPOINT s2; +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 s2; +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) + +CLOSE c; +COMMIT; +-- the same for nested subtransactions +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +SAVEPOINT s3; +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) + +RELEASE SAVEPOINT s3; +ROLLBACK TO s2; +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) + +CLOSE c; +COMMIT; +-- with a small fetch size, no more rows can be fetched if the subtransaction +-- the remote cursor was created in has been rolled back, however deep it was +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE SAVEPOINT s1; +SAVEPOINT s2; +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 s2; +FETCH c; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +SAVEPOINT s3; +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) + +RELEASE SAVEPOINT s3; +ROLLBACK TO s2; +FETCH c; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +-- but the cursor is fine if the subtransactions are released +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +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) + +RELEASE SAVEPOINT s2; +RELEASE SAVEPOINT s1; +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) + +CLOSE c; +COMMIT; +-- a remote cursor is created within the subtransaction in which parameter +-- evaluation happens, which may involve other foreign scans +BEGIN; +DECLARE c CURSOR FOR +SELECT c1 FROM ft1 WHERE c1 = (SELECT c1 FROM ft2 ORDER BY c1 LIMIT 1); +SAVEPOINT s; +FETCH c; + c1 +---- + 1 +(1 row) + +ROLLBACK TO s; +FETCH c; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +-- the same with a cursor opened in PL/pgSQL, fetched within a block +DO $$ +DECLARE c refcursor; r record; n int := 0; +BEGIN + OPEN c FOR SELECT c1 FROM ft1 ORDER BY c1; + BEGIN + FETCH c INTO r; n := n + 1; + RAISE EXCEPTION 'x'; + EXCEPTION WHEN others THEN NULL; + END; + FETCH c INTO r; n := n + 1; + CLOSE c; + RAISE NOTICE 'n = %', n; +END $$; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +CONTEXT: PL/pgSQL function inline_code_block line 10 at FETCH +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); +-- ... which works if the rows are already fetched +DO $$ +DECLARE c refcursor; r record; n int := 0; +BEGIN + OPEN c FOR SELECT c1 FROM ft1 ORDER BY c1; + BEGIN + FETCH c INTO r; n := n + 1; + RAISE EXCEPTION 'x'; + EXCEPTION WHEN others THEN NULL; + END; + FETCH c INTO r; n := n + 1; + CLOSE c; + RAISE NOTICE 'n = %', n; +END $$; +NOTICE: n = 2 +-- an asynchronous foreign scan whose cursor was lost +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1', ADD async_capable 'true'); +EXPLAIN (COSTS OFF) +SELECT c1 FROM ft1 UNION ALL SELECT c1 FROM ft1 WHERE c1 < 0; + QUERY PLAN +--------------------------------------- + Append + -> Async Foreign Scan on ft1 + -> Async Foreign Scan on ft1 ft1_1 +(3 rows) + +BEGIN; +DECLARE c CURSOR FOR +SELECT c1 FROM ft1 UNION ALL SELECT c1 FROM ft1 WHERE c1 < 0; +SAVEPOINT s; +FETCH c; + c1 +---- + 1 +(1 row) + +ROLLBACK TO s; +FETCH ALL c; +ERROR: cannot fetch more rows from foreign table "ft1" +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size, DROP async_capable); +-- the same applies even if the scan is for a pushed-down join +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1'); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST +(4 rows) + +BEGIN; +DECLARE c CURSOR FOR +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; +SAVEPOINT s; +FETCH c; + c1 | c1 +----+---- + 1 | 1 +(1 row) + +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; +ERROR: cannot fetch more rows from this foreign scan +DETAIL: The remote cursor was removed by rolling back the subtransaction in which it was created. +ABORT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); +ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size); +-- an error while a cursor is fetched within a savepoint leaves the portal in +-- the failed state, so the scan is not shut down; make sure we cope with that +BEGIN; +DECLARE c CURSOR FOR SELECT 1 / (c1 - 3) FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH 5 c; -- error +ERROR: division by zero +ROLLBACK TO s; +CLOSE c; +SAVEPOINT s; +RELEASE SAVEPOINT s; +SELECT count(*) FROM ft1; + count +------- + 1000 +(1 row) + +COMMIT; +BEGIN; +DECLARE c CURSOR FOR SELECT 1 / (c1 - 3) FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH 5 c; -- error +ERROR: division by zero +ROLLBACK TO s; COMMIT; -- =================================================================== -- test handling of collations diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 4ca061c9e0b..2e5886059dd 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -17,6 +17,7 @@ #include "access/htup_details.h" #include "access/sysattr.h" #include "access/table.h" +#include "access/xact.h" #include "catalog/pg_opfamily.h" #include "commands/defrem.h" #include "commands/explain_format.h" @@ -185,6 +186,10 @@ typedef struct PgFdwScanState PgFdwConnState *conn_state; /* extra per-connection state */ unsigned int cursor_number; /* quasi-unique ID for my cursor */ bool cursor_exists; /* have we created the cursor? */ + int cursor_level; /* nesting level of the (sub)transaction in + * which the remote cursor was declared, which + * is also the depth of the remote savepoint + * holding it; 0 if no cursor exists */ int numParams; /* number of parameters passed to query */ FmgrInfo *param_flinfo; /* output conversion functions for them */ List *param_exprs; /* executable expressions for param values */ @@ -207,8 +212,46 @@ typedef struct PgFdwScanState MemoryContext temp_cxt; /* context for per-tuple temporary data */ int fetch_size; /* number of tuples per fetch */ + + bool cursor_lost; /* true if the remote cursor was removed by a + * rolled-back savepoint; the tuples already + * fetched are still usable, but no more can + * be fetched; see AtEOSubXact_ForeignScan */ } PgFdwScanState; +/* + * Registry of the foreign scans that are currently open in this backend. + * + * A remote cursor is declared inside the remote savepoint that matches the + * local (sub)transaction in which the first FETCH happens. If that local + * subtransaction is rolled back, the remote cursor is destroyed along with + * its remote savepoint, but the local scan, which may well belong to an + * outer (sub)transaction (e.g., a cursor opened before the savepoint), does + * not go away and would keep using a cursor that no longer exists. + * + * The subtransaction callbacks (see pgfdw_subxact_callback) are the only + * place where we learn about this, but they have no way to reach the scan + * nodes of the executor, so each scan registers itself here. On + * subtransaction commit, AtEOSubXact_ForeignScan hands the cursors declared + * in the subtransaction over to its parent, mirroring the remote RELEASE; on + * abort, it marks them as lost. + * + * Entries point into executor memory, so they must never outlive the scan + * node. Normally postgresEndForeignScan removes the entry, but the executor + * does not call EndForeignScan for a scan whose portal was marked failed by + * an aborted subtransaction, e.g., when an error occurred while the scan was + * active. To cover such cases, every scan also registers a callback on its + * memory context that removes the entry (see ForeignScanContextCallback). + * + * Initialized on first use. + */ +typedef struct ForeignScanEntry +{ + ForeignScanState *fs_node; /* hash key */ +} ForeignScanEntry; + +static HTAB *ForeignScanHash = NULL; + /* * Execution state of a foreign insert/update/delete operation. */ @@ -539,6 +582,10 @@ static void create_cursor(ForeignScanState *node); static void fetch_more_data(ForeignScanState *node); static void close_cursor(PGconn *conn, unsigned int cursor_number, PgFdwConnState *conn_state); +static void ForeignScanHashInsert(ForeignScanState *node); +static void ForeignScanHashDelete(ForeignScanState *node); +static void ForeignScanContextCallback(void *arg); +static void check_cursor_not_lost(PgFdwScanState *fsstate); static PgFdwModifyState *create_foreign_modify(EState *estate, RangeTblEntry *rte, ResultRelInfo *resultRelInfo, @@ -1703,6 +1750,7 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan; EState *estate = node->ss.ps.state; PgFdwScanState *fsstate; + MemoryContextCallback *scan_cb; RangeTblEntry *rte; Oid userid; ForeignTable *table; @@ -1762,6 +1810,7 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) /* Assign a unique ID for my cursor */ fsstate->cursor_number = GetCursorNumber(fsstate->conn); fsstate->cursor_exists = false; + fsstate->cursor_level = 0; /* Get private info created by planner functions. */ fsstate->query = strVal(list_nth(fsplan->fdw_private, @@ -1822,6 +1871,20 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) /* Set the async-capable flag */ fsstate->async_capable = node->ss.ps.async_capable; + + fsstate->cursor_lost = false; + + /* + * Register the scan, and make sure that it is unregistered even if + * EndForeignScan is never called for it. The callback is allocated in + * the context it is registered on, so that it can't outlive it. + */ + ForeignScanHashInsert(node); + scan_cb = (MemoryContextCallback *) + MemoryContextAlloc(estate->es_query_cxt, sizeof(MemoryContextCallback)); + scan_cb->func = ForeignScanContextCallback; + scan_cb->arg = node; + MemoryContextRegisterResetCallback(estate->es_query_cxt, scan_cb); } /* @@ -1892,6 +1955,30 @@ postgresReScanForeignScan(ForeignScanState *node) Assert(!fsstate->async_capable || !fsstate->conn_state->pendingAreq || fsstate->conn_state->pendingAreq->requestee != (PlanState *) node); + /* + * If the remote cursor is gone, there is nothing to close or rewind on + * the remote side. If we can't just rescan what we already have in + * memory, forget the cursor, so that a fresh one is created at the next + * fetch. + */ + if (fsstate->cursor_lost) + { + if (node->ss.ps.chgParam == NULL && + fsstate->fetch_ct_2 <= 1 && fsstate->eof_reached) + fsstate->next_tuple = 0; + else + { + fsstate->cursor_exists = false; + fsstate->cursor_lost = false; + fsstate->tuples = NULL; + fsstate->num_tuples = 0; + fsstate->next_tuple = 0; + fsstate->fetch_ct_2 = 0; + fsstate->eof_reached = false; + } + return; + } + /* * If any internal parameters affecting this node have changed, we'd * better destroy and recreate the cursor. Otherwise, if the remote @@ -1904,6 +1991,7 @@ postgresReScanForeignScan(ForeignScanState *node) if (node->ss.ps.chgParam != NULL) { fsstate->cursor_exists = false; + fsstate->cursor_level = 0; snprintf(sql, sizeof(sql), "CLOSE c%u", fsstate->cursor_number); } @@ -1915,6 +2003,7 @@ postgresReScanForeignScan(ForeignScanState *node) else { fsstate->cursor_exists = false; + fsstate->cursor_level = 0; snprintf(sql, sizeof(sql), "CLOSE c%u", fsstate->cursor_number); } @@ -1952,8 +2041,11 @@ postgresEndForeignScan(ForeignScanState *node) if (fsstate == NULL) return; - /* Close the cursor if open, to prevent accumulation of cursors */ - if (fsstate->cursor_exists) + /* + * Close the cursor if open, to prevent accumulation of cursors; but not + * if it's already gone. + */ + if (fsstate->cursor_exists && !fsstate->cursor_lost) close_cursor(fsstate->conn, fsstate->cursor_number, fsstate->conn_state); @@ -1961,6 +2053,8 @@ postgresEndForeignScan(ForeignScanState *node) ReleaseConnection(fsstate->conn); fsstate->conn = NULL; + ForeignScanHashDelete(node); + /* MemoryContexts will be deleted automatically. */ } @@ -4058,6 +4152,12 @@ create_cursor(ForeignScanState *node) 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 @@ -4103,6 +4203,8 @@ create_cursor(ForeignScanState *node) /* Mark the cursor as created, and show no tuples have been retrieved */ fsstate->cursor_exists = true; + fsstate->cursor_lost = false; + fsstate->cursor_level = GetCurrentTransactionNestLevel(); fsstate->tuples = NULL; fsstate->num_tuples = 0; fsstate->next_tuple = 0; @@ -4126,6 +4228,12 @@ fetch_more_data(ForeignScanState *node) int i; MemoryContext oldcontext; + /* + * Don't throw away the previous batch if we can't fetch another one. + */ + if (!fsstate->async_capable) + check_cursor_not_lost(fsstate); + /* * We'll store the tuples in the batch_cxt. First, flush the previous * batch. @@ -4147,7 +4255,7 @@ fetch_more_data(ForeignScanState *node) if (PQresultStatus(res) != PGRES_TUPLES_OK) pgfdw_report_error(res, conn, fsstate->query); - /* Reset per-connection state */ + /* Reset the pending asynchronous request */ fsstate->conn_state->pendingAreq = NULL; } else @@ -4270,6 +4378,165 @@ close_cursor(PGconn *conn, unsigned int cursor_number, PQclear(res); } +/* + * Complain if the remote cursor of a scan was removed by rolling back a + * savepoint, in which case no more tuples can be fetched for the scan. + * + * Note that fsstate->rel is NULL if the scan is for a join or an upper + * relation pushed down to the remote server. + */ +static void +check_cursor_not_lost(PgFdwScanState *fsstate) +{ + if (!fsstate->cursor_lost) + return; + + if (fsstate->rel != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot fetch more rows from foreign table \"%s\"", + RelationGetRelationName(fsstate->rel)), + errdetail("The remote cursor was removed by rolling back the subtransaction in which it was created."))); + else + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot fetch more rows from this foreign scan"), + errdetail("The remote cursor was removed by rolling back the subtransaction in which it was created."))); +} + +/* + * Add a ForeignScan node to the ForeignScanHash table + */ +static void +ForeignScanHashInsert(ForeignScanState *node) +{ + ForeignScanEntry *entry; + bool found; + + /* First time through, initialize foreign scan hashtable */ + if (ForeignScanHash == NULL) + { + HASHCTL ctl; + + ctl.keysize = sizeof(ForeignScanState *); + ctl.entrysize = sizeof(ForeignScanEntry); + ForeignScanHash = hash_create("postgres_fdw ForeignScan nodes", + 256L, + &ctl, + HASH_ELEM | HASH_BLOBS); + } + + entry = (ForeignScanEntry *) hash_search(ForeignScanHash, + &node, + HASH_ENTER, + &found); + if (found) + elog(ERROR, "duplicate ForeignScan node"); + entry->fs_node = node; +} + +/* + * Remove a ForeignScan node from the ForeignScanHash table, if it is there + */ +static void +ForeignScanHashDelete(ForeignScanState *node) +{ + if (ForeignScanHash == NULL) + return; + + (void) hash_search(ForeignScanHash, &node, HASH_REMOVE, NULL); +} + +/* + * Memory context callback to unregister a foreign scan whose executor memory + * is being freed. This must not throw errors. + */ +static void +ForeignScanContextCallback(void *arg) +{ + ForeignScanHashDelete((ForeignScanState *) arg); +} + +/* + * Clean up foreign scans after a transaction commit/abort + */ +void +AtEOXact_ForeignScan(void) +{ + HASH_SEQ_STATUS scan; + ForeignScanEntry *entry; + + if (ForeignScanHash == NULL) + return; + + hash_seq_init(&scan, ForeignScanHash); + while ((entry = hash_seq_search(&scan)) != NULL) + { + ForeignScanState *node = entry->fs_node; + + /* + * Scans of portals that were marked failed may still be around at + * this point; their memory is freed later, at which point the context + * callback finds nothing to remove. + */ + ForeignScanHashDelete(node); + } +} + +/* + * Clean up foreign scans after a subtransaction subcommit/subabort + * + * "level" is the nesting level of the subtransaction that ended. + * + * We only care about the remote cursors that were declared in that + * subtransaction, i.e., those whose cursor_level is at least "level" (it can't + * be deeper, since deeper subtransactions have already ended and handed their + * cursors over to this one). + * + * On subcommit, the remote savepoint of that level is released, so such + * cursors now belong to the parent level. + * + * On subabort, the remote savepoint is rolled back, so such cursors are gone + * on the remote server. Mark them as lost. The tuples the scan has already + * fetched remain usable, but trying to fetch more fails with a clear error + * (see check_cursor_not_lost), rather than with a remote "cursor does not + * exist" error at some later point, such as when the cursor is closed. Note + * that we needn't tell apart the scans created in the aborted subtransaction, + * which are going away anyway; marking their cursors is harmless. + */ +void +AtEOSubXact_ForeignScan(int level, bool is_subcommit) +{ + HASH_SEQ_STATUS scan; + ForeignScanEntry *entry; + + if (ForeignScanHash == NULL) + return; + + hash_seq_init(&scan, ForeignScanHash); + while ((entry = hash_seq_search(&scan)) != NULL) + { + ForeignScanState *node = entry->fs_node; + PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state; + + /* Skip unless the remote cursor was declared in this subxact */ + if (!fsstate->cursor_exists || fsstate->cursor_level < level) + continue; + + if (is_subcommit) + { + /* The cursor now belongs to the parent subxact */ + fsstate->cursor_level = level - 1; + } + else + { + /* The cursor is gone; remember that */ + fsstate->cursor_lost = true; + fsstate->cursor_level = 0; + } + } +} + /* * create_foreign_modify * Construct an execution state of a foreign insert/update/delete @@ -4394,6 +4661,9 @@ create_foreign_modify(EState *estate, * result if any. (This is the shared guts of postgresExecForeignInsert, * postgresExecForeignBatchInsert, postgresExecForeignUpdate, and * postgresExecForeignDelete.) + * + * Note: this function is never called from open cursors, thus no need to + * synchronize the local/remote transactions. */ static TupleTableSlot ** execute_foreign_modify(EState *estate, @@ -4832,6 +5102,9 @@ rebuild_fdw_scan_tlist(ForeignScan *fscan, List *tlist) /* * Execute a direct UPDATE/DELETE statement. + * + * Note: this function is never called from open cursors, thus no need to + * synchronize the local/remote transactions. */ static void execute_dml_stmt(ForeignScanState *node) @@ -8840,9 +9113,16 @@ fetch_more_data_begin(AsyncRequest *areq) Assert(!fsstate->conn_state->pendingAreq); - /* Create the cursor synchronously. */ + check_cursor_not_lost(fsstate); + + /* + * Create the cursor synchronously if not already done. Otherwise, + * synchronize the local/remote transactions before the data fetch. + */ if (!fsstate->cursor_exists) create_cursor(node); + else + pgfdw_begin_remote_xact(fsstate->conn_state->entry); /* We will send this query, but not wait for the response. */ snprintf(sql, sizeof(sql), "FETCH %d FROM c%u", diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index da7da1c2ea9..7ed1fb2c8e5 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -147,6 +147,7 @@ typedef struct PgFdwRelationInfo typedef struct PgFdwConnState { AsyncRequest *pendingAreq; /* pending async request */ + struct ConnCacheEntry *entry; /* link to containing ConnCacheEntry */ } PgFdwConnState; /* @@ -165,6 +166,8 @@ 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 AtEOXact_ForeignScan(void); +extern void AtEOSubXact_ForeignScan(int level, bool is_subcommit); /* in connection.c */ extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt, @@ -173,6 +176,7 @@ extern void ReleaseConnection(PGconn *conn); extern unsigned int GetCursorNumber(PGconn *conn); 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 PGresult *pgfdw_get_result(PGconn *conn); extern PGresult *pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state); diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 77c9a25a890..3ca6d467d6c 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -1640,6 +1640,9 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE user_enum; -- =================================================================== -- subtransaction -- + local/remote error doesn't break cursor +-- + we allow cursors opened before a savepoint to be first fetched +-- after the savepoint; if that subtransaction aborts, the rows already +-- fetched can still be returned, but no more can be fetched. -- =================================================================== BEGIN; DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; @@ -1655,6 +1658,226 @@ FETCH c; SELECT * FROM ft1 ORDER BY c1 LIMIT 1; COMMIT; +-- the rows fetched in the first batch are still available +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; +CLOSE c; +COMMIT; + +-- but fetching another batch is not possible +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; +ABORT; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +RELEASE SAVEPOINT s; +CLOSE c; +COMMIT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); + +-- a rescan of a scan whose cursor was lost creates a new one +SET enable_hashjoin = off; +SET enable_mergejoin = off; +SET enable_material = off; +EXPLAIN (COSTS OFF) +SELECT * FROM (VALUES (1), (2), (3)) v(x), + LATERAL (SELECT c1 FROM ft1 WHERE c1 = v.x OFFSET 0) s; +BEGIN; +DECLARE c CURSOR FOR +SELECT * FROM (VALUES (1), (2), (3)) v(x), + LATERAL (SELECT c1 FROM ft1 WHERE c1 = v.x OFFSET 0) s; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +FETCH ALL c; +COMMIT; +RESET enable_hashjoin; +RESET enable_mergejoin; +RESET enable_material; + +-- cursor created in a subtransaction is handed to its parent subtransaction +BEGIN; +SAVEPOINT s; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE SAVEPOINT s; +FETCH c; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; +CLOSE c; +COMMIT; + +-- a cursor created in a subtransaction, which is released, can be first +-- fetched in a sibling subtransaction that is then rolled back +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE SAVEPOINT s1; +SAVEPOINT s2; +FETCH c; +ROLLBACK TO s2; +FETCH c; +CLOSE c; +COMMIT; + +-- the same for nested subtransactions +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +SAVEPOINT s3; +FETCH c; +RELEASE SAVEPOINT s3; +ROLLBACK TO s2; +FETCH c; +CLOSE c; +COMMIT; + +-- with a small fetch size, no more rows can be fetched if the subtransaction +-- the remote cursor was created in has been rolled back, however deep it was +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +BEGIN; +SAVEPOINT s1; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +RELEASE SAVEPOINT s1; +SAVEPOINT s2; +FETCH c; +ROLLBACK TO s2; +FETCH c; +ABORT; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +SAVEPOINT s3; +FETCH c; +RELEASE SAVEPOINT s3; +ROLLBACK TO s2; +FETCH c; +ABORT; + +-- but the cursor is fine if the subtransactions are released +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s1; +SAVEPOINT s2; +FETCH c; +RELEASE SAVEPOINT s2; +RELEASE SAVEPOINT s1; +FETCH c; +CLOSE c; +COMMIT; + +-- a remote cursor is created within the subtransaction in which parameter +-- evaluation happens, which may involve other foreign scans +BEGIN; +DECLARE c CURSOR FOR +SELECT c1 FROM ft1 WHERE c1 = (SELECT c1 FROM ft2 ORDER BY c1 LIMIT 1); +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +FETCH c; +ABORT; + +-- the same with a cursor opened in PL/pgSQL, fetched within a block +DO $$ +DECLARE c refcursor; r record; n int := 0; +BEGIN + OPEN c FOR SELECT c1 FROM ft1 ORDER BY c1; + BEGIN + FETCH c INTO r; n := n + 1; + RAISE EXCEPTION 'x'; + EXCEPTION WHEN others THEN NULL; + END; + FETCH c INTO r; n := n + 1; + CLOSE c; + RAISE NOTICE 'n = %', n; +END $$; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); + +-- ... which works if the rows are already fetched +DO $$ +DECLARE c refcursor; r record; n int := 0; +BEGIN + OPEN c FOR SELECT c1 FROM ft1 ORDER BY c1; + BEGIN + FETCH c INTO r; n := n + 1; + RAISE EXCEPTION 'x'; + EXCEPTION WHEN others THEN NULL; + END; + FETCH c INTO r; n := n + 1; + CLOSE c; + RAISE NOTICE 'n = %', n; +END $$; + +-- an asynchronous foreign scan whose cursor was lost +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1', ADD async_capable 'true'); +EXPLAIN (COSTS OFF) +SELECT c1 FROM ft1 UNION ALL SELECT c1 FROM ft1 WHERE c1 < 0; +BEGIN; +DECLARE c CURSOR FOR +SELECT c1 FROM ft1 UNION ALL SELECT c1 FROM ft1 WHERE c1 < 0; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +FETCH ALL c; +ABORT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size, DROP async_capable); + +-- the same applies even if the scan is for a pushed-down join +ALTER FOREIGN TABLE ft1 OPTIONS (ADD fetch_size '1'); +ALTER FOREIGN TABLE ft2 OPTIONS (ADD fetch_size '1'); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; +BEGIN; +DECLARE c CURSOR FOR +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; +SAVEPOINT s; +FETCH c; +ROLLBACK TO s; +RELEASE SAVEPOINT s; +FETCH c; +ABORT; +ALTER FOREIGN TABLE ft1 OPTIONS (DROP fetch_size); +ALTER FOREIGN TABLE ft2 OPTIONS (DROP fetch_size); + +-- an error while a cursor is fetched within a savepoint leaves the portal in +-- the failed state, so the scan is not shut down; make sure we cope with that +BEGIN; +DECLARE c CURSOR FOR SELECT 1 / (c1 - 3) FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH 5 c; -- error +ROLLBACK TO s; +CLOSE c; +SAVEPOINT s; +RELEASE SAVEPOINT s; +SELECT count(*) FROM ft1; +COMMIT; +BEGIN; +DECLARE c CURSOR FOR SELECT 1 / (c1 - 3) FROM ft1 ORDER BY c1; +SAVEPOINT s; +FETCH 5 c; -- error +ROLLBACK TO s; +COMMIT; + -- =================================================================== -- test handling of collations -- =================================================================== diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a7a8dbdd23f..6c4d1096b59 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -887,6 +887,7 @@ ForeignKeyCacheInfo ForeignKeyOptInfo ForeignPath ForeignScan +ForeignScanEntry ForeignScanState ForeignServer ForeignServerInfo -- 2.50.1 (Apple Git-155)