diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 652fa4a943d..77aec0fd9eb 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -112,6 +112,20 @@ static uint32 pgfdw_we_get_result = 0; */ #define RETRY_CANCEL_TIMEOUT 1000 +/* + * Macro for constructing commit command to be sent + * + * We synchronize the read/write mode before committing remote transactions + * so deferred triggers on remote servers can run in the right mode. + */ +#define CONSTRUCT_COMMIT_COMMAND(sql, entry) \ + do { \ + if ((read_only_level > 0) && !(entry)->xact_read_only) \ + strcpy((sql), "SET TRANSACTION READ ONLY; COMMIT TRANSACTION"); \ + else \ + strcpy((sql), "COMMIT TRANSACTION"); \ + } while(0) + /* Macro for constructing abort command to be sent */ #define CONSTRUCT_ABORT_COMMAND(sql, entry, toplevel) \ do { \ @@ -942,7 +956,7 @@ begin_remote_xact(ConnCacheEntry *entry) appendStringInfoString(&sql, "REPEATABLE READ"); if (ro) appendStringInfoString(&sql, " READ ONLY"); - if (XactDeferrable) + if (XactDeferrable && PQserverVersion(entry->conn) >= 90100) appendStringInfoString(&sql, " DEFERRABLE"); entry->changing_xact_state = true; do_sql_command(entry->conn, sql.data); @@ -972,7 +986,7 @@ begin_remote_xact(ConnCacheEntry *entry) if (entry->xact_depth == read_only_level) { entry->changing_xact_state = true; - do_sql_command(entry->conn, "SET transaction_read_only = on"); + do_sql_command(entry->conn, "SET TRANSACTION READ ONLY"); entry->xact_read_only = true; entry->changing_xact_state = false; } @@ -1005,7 +1019,7 @@ begin_remote_xact(ConnCacheEntry *entry) initStringInfo(&sql); appendStringInfo(&sql, "SAVEPOINT s%d", entry->xact_depth + 1); if (ro) - appendStringInfoString(&sql, "; SET transaction_read_only = on"); + appendStringInfoString(&sql, "; SET TRANSACTION READ ONLY"); entry->changing_xact_state = true; do_sql_command(entry->conn, sql.data); entry->xact_depth++; @@ -1206,6 +1220,24 @@ pgfdw_xact_callback(XactEvent event, void *arg) if (!xact_got_connection) return; + /* + * If we are called for pre-commit cleanup, ensure read_only_level is set + * for later processing. Note that we need to do this because the local + * transaction may have become read-only since the last remote operation. + */ + if (event == XACT_EVENT_PARALLEL_PRE_COMMIT || + event == XACT_EVENT_PRE_COMMIT) + { + if (XactReadOnly) + { + if (read_only_level == 0) + read_only_level = 1; + Assert(read_only_level == 1); + } + else + Assert(read_only_level == 0); + } + /* * Scan all connection cache entries to find open remote transactions, and * close them. @@ -1222,6 +1254,8 @@ pgfdw_xact_callback(XactEvent event, void *arg) /* If it has an open remote transaction, try to close it */ if (entry->xact_depth > 0) { + char sql[100]; + elog(DEBUG3, "closing remote transaction on connection %p", entry->conn); @@ -1237,14 +1271,17 @@ pgfdw_xact_callback(XactEvent event, void *arg) pgfdw_reject_incomplete_xact_state_change(entry); /* Commit all remote transactions during pre-commit */ + CONSTRUCT_COMMIT_COMMAND(sql, entry); entry->changing_xact_state = true; if (entry->parallel_commit) { - do_sql_command_begin(entry->conn, "COMMIT TRANSACTION"); + do_sql_command_begin(entry->conn, sql); pending_entries = lappend(pending_entries, entry); continue; } - do_sql_command(entry->conn, "COMMIT TRANSACTION"); + do_sql_command(entry->conn, sql); + if ((read_only_level > 0) && !entry->xact_read_only) + entry->xact_read_only = true; entry->changing_xact_state = false; /* @@ -2041,6 +2078,8 @@ pgfdw_finish_pre_commit_cleanup(List *pending_entries) */ foreach(lc, pending_entries) { + char sql[100]; + entry = (ConnCacheEntry *) lfirst(lc); Assert(entry->changing_xact_state); @@ -2049,7 +2088,10 @@ pgfdw_finish_pre_commit_cleanup(List *pending_entries) * We might already have received the result on the socket, so pass * consume_input=true to try to consume it first */ - do_sql_command_end(entry->conn, "COMMIT TRANSACTION", true); + CONSTRUCT_COMMIT_COMMAND(sql, entry); + do_sql_command_end(entry->conn, sql, true); + if ((read_only_level > 0) && !(entry)->xact_read_only) + entry->xact_read_only = true; entry->changing_xact_state = false; /* Do a DEALLOCATE ALL in parallel if needed */ diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 3aab56b0642..6c87a27243b 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -13373,6 +13373,7 @@ DROP VIEW my_application_name; -- test read-only and/or deferrable transactions -- =================================================================== CREATE TABLE loct (f1 int, f2 text); +INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS 'UPDATE public.loct SET f2 = f2 || f2 RETURNING *'; CREATE VIEW locv AS SELECT t.* FROM locf() t; @@ -13380,7 +13381,6 @@ CREATE FOREIGN TABLE remt (f1 int, f2 text) SERVER loopback OPTIONS (table_name 'locv'); CREATE FOREIGN TABLE remt2 (f1 int, f2 text) SERVER loopback2 OPTIONS (table_name 'locv'); -INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); START TRANSACTION READ ONLY; SAVEPOINT s; SELECT * FROM remt; -- should fail @@ -13469,9 +13469,32 @@ ERROR: cannot execute UPDATE in a read-only transaction CONTEXT: SQL function "locf" statement 1 remote SQL command: SELECT f1, f2 FROM public.locv ROLLBACK; +-- Clean up DROP FOREIGN TABLE remt; +DROP FOREIGN TABLE remt2; +DROP VIEW locv; +DROP FUNCTION locf(); CREATE FOREIGN TABLE remt (f1 int, f2 text) SERVER loopback OPTIONS (table_name 'loct'); +CREATE FUNCTION defer_trig_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.f2 IS NOT NULL THEN + UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1; + END IF; + RETURN NULL; +END; +$$; +CREATE CONSTRAINT TRIGGER defer_trig AFTER INSERT ON loct + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE defer_trig_func(); +START TRANSACTION; +INSERT INTO remt VALUES (3, 'baz'); +SET TRANSACTION READ ONLY; +COMMIT; +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL statement "UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1" +PL/pgSQL function public.defer_trig_func() line 4 at SQL statement +remote SQL command: SET TRANSACTION READ ONLY; COMMIT TRANSACTION START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY; SELECT * FROM remt; f1 | f2 @@ -13501,9 +13524,8 @@ SELECT * FROM remt; COMMIT; -- Clean up DROP FOREIGN TABLE remt; -DROP FOREIGN TABLE remt2; -DROP VIEW locv; -DROP FUNCTION locf(); +DROP TRIGGER defer_trig ON loct; +DROP FUNCTION defer_trig_func; DROP TABLE loct; -- =================================================================== -- test parallel commit and parallel abort diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 9c271953206..0b313ebb418 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4659,6 +4659,8 @@ DROP VIEW my_application_name; -- test read-only and/or deferrable transactions -- =================================================================== CREATE TABLE loct (f1 int, f2 text); +INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); + CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS 'UPDATE public.loct SET f2 = f2 || f2 RETURNING *'; CREATE VIEW locv AS SELECT t.* FROM locf() t; @@ -4666,7 +4668,6 @@ CREATE FOREIGN TABLE remt (f1 int, f2 text) SERVER loopback OPTIONS (table_name 'locv'); CREATE FOREIGN TABLE remt2 (f1 int, f2 text) SERVER loopback2 OPTIONS (table_name 'locv'); -INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); START TRANSACTION READ ONLY; SAVEPOINT s; @@ -4712,10 +4713,32 @@ SET transaction_read_only = on; SELECT * FROM remt2; -- should fail ROLLBACK; +-- Clean up DROP FOREIGN TABLE remt; +DROP FOREIGN TABLE remt2; +DROP VIEW locv; +DROP FUNCTION locf(); + CREATE FOREIGN TABLE remt (f1 int, f2 text) SERVER loopback OPTIONS (table_name 'loct'); +CREATE FUNCTION defer_trig_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.f2 IS NOT NULL THEN + UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1; + END IF; + RETURN NULL; +END; +$$; +CREATE CONSTRAINT TRIGGER defer_trig AFTER INSERT ON loct + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE defer_trig_func(); + +START TRANSACTION; +INSERT INTO remt VALUES (3, 'baz'); +SET TRANSACTION READ ONLY; +COMMIT; + START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY; SELECT * FROM remt; COMMIT; @@ -4730,9 +4753,8 @@ COMMIT; -- Clean up DROP FOREIGN TABLE remt; -DROP FOREIGN TABLE remt2; -DROP VIEW locv; -DROP FUNCTION locf(); +DROP TRIGGER defer_trig ON loct; +DROP FUNCTION defer_trig_func; DROP TABLE loct; -- =================================================================== diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index fe4e6478e28..01577d8d69b 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1148,20 +1148,17 @@ CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testp - The remote transaction is opened in the same read/write mode as the local - transaction: if the local transaction is READ ONLY, - the remote transaction is opened in READ ONLY mode, - otherwise it is opened in READ WRITE mode. - (This rule is also applied to remote and local subtransactions.) + Local READ ONLY transactions propagate their read-only + mode to remote sessions. + (This rule is also applied to local subtransactions.) Note that this does not prevent login triggers executed on the remote server from writing. - The remote transaction is also opened in the same deferrable mode as the - local transaction: if the local transaction is DEFERRABLE, - the remote transaction is opened in DEFERRABLE mode, - otherwise it is opened in NOT DEFERRABLE mode. + Also, local DEFERRABLE transactions propagate their + deferrable mode to remote sessions. + (This rule is only applied to remote servers 9.1 and newer.)