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.)