]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
postgres_fdw: Inherit the local transaction's access/deferrable modes.
authorEtsuro Fujita <efujita@postgresql.org>
Sun, 5 Apr 2026 09:55:00 +0000 (18:55 +0900)
committerEtsuro Fujita <efujita@postgresql.org>
Sun, 5 Apr 2026 09:55:00 +0000 (18:55 +0900)
READ ONLY transactions should prevent modifications to foreign data as
well as local data, but postgres_fdw transactions declared as READ ONLY
that reference foreign tables mapped to a remote view executing volatile
functions would modify data on remote servers, as it would open remote
transactions in READ WRITE mode.

Similarly, DEFERRABLE transactions should not abort due to a
serialization failure even when accessing foreign data, but postgres_fdw
transactions declared as DEFERRABLE would abort due to that failure in a
remote server, as it would open remote transactions in NOT DEFERRABLE
mode.

To fix, modify postgres_fdw to open remote transactions in the same
access/deferrable modes as the local transaction.  This commit also
modifies it to open remote subtransactions in the same access mode as
the local subtransaction.

This commit changes the behavior of READ ONLY/DEFERRABLE transactions
using postgres_fdw; in particular, it doesn't allow the READ ONLY
transactions to modify data on remote servers anymore, so such
transactions should be redeclared as READ WRITE or rewritten using other
tools like dblink.  The release notes should note this as an
incompatibility.

These issues exist since the introduction of postgres_fdw, but to avoid
the incompatibility in the back branches, fix them in master only.

Author: Etsuro Fujita <etsuro.fujita@gmail.com>
Reviewed-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/CAPmGK16n_hcUUWuOdmeUS%2Bw4Q6dZvTEDHb%3DOP%3D5JBzo-M3QmpQ%40mail.gmail.com
Discussion: https://postgr.es/m/E1uLe9X-000zsY-2g%40gemulon.postgresql.org

contrib/postgres_fdw/connection.c
contrib/postgres_fdw/expected/postgres_fdw.out
contrib/postgres_fdw/sql/postgres_fdw.sql
doc/src/sgml/postgres-fdw.sgml
src/backend/access/transam/xact.c
src/include/access/xact.h

index 192f8011160a99a3af7421c2cc39b0bf8a10b121..06673017bcfafe986a73244d844db8720851ba25 100644 (file)
@@ -60,6 +60,7 @@ typedef struct ConnCacheEntry
        /* Remaining fields are invalid when conn is NULL: */
        int                     xact_depth;             /* 0 = no xact open, 1 = main xact open, 2 =
                                                                 * one level of subxact open, etc */
+       bool            xact_read_only; /* xact r/o state */
        bool            have_prep_stmt; /* have we prepared any stmts in this xact? */
        bool            have_error;             /* have any subxacts aborted in this xact? */
        bool            changing_xact_state;    /* xact state change in process */
@@ -86,6 +87,12 @@ static unsigned int prep_stmt_number = 0;
 /* tracks whether any work is needed in callback functions */
 static bool xact_got_connection = false;
 
+/*
+ * tracks the topmost read-only local transaction's nesting level determined
+ * by GetTopReadOnlyTransactionNestLevel()
+ */
+static int     read_only_level = 0;
+
 /* custom wait event values, retrieved from shared memory */
 static uint32 pgfdw_we_cleanup_result = 0;
 static uint32 pgfdw_we_connect = 0;
@@ -378,6 +385,7 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user)
 
        /* Reset all transient state fields, to be sure all are clean */
        entry->xact_depth = 0;
+       entry->xact_read_only = false;
        entry->have_prep_stmt = false;
        entry->have_error = false;
        entry->changing_xact_state = false;
@@ -871,29 +879,106 @@ do_sql_command_end(PGconn *conn, const char *sql, bool consume_input)
  * those scans.  A disadvantage is that we can't provide sane emulation of
  * READ COMMITTED behavior --- it would be nice if we had some other way to
  * control which remote queries share a snapshot.
+ *
+ * Note also that we always start the remote transaction with the same
+ * read/write and deferrable properties as the local transaction, and start
+ * the remote subtransaction with the same read/write property as the local
+ * subtransaction.
  */
 static void
 begin_remote_xact(ConnCacheEntry *entry)
 {
        int                     curlevel = GetCurrentTransactionNestLevel();
 
-       /* Start main transaction if we haven't yet */
+       /*
+        * If the current local (sub)transaction is read-only, set the topmost
+        * read-only local transaction's nesting level if we haven't yet.
+        *
+        * Note: once it's set, it's retained until the topmost read-only local
+        * transaction is committed/aborted (see pgfdw_xact_callback and
+        * pgfdw_subxact_callback).
+        */
+       if (XactReadOnly)
+       {
+               if (read_only_level == 0)
+                       read_only_level = GetTopReadOnlyTransactionNestLevel();
+               Assert(read_only_level > 0);
+       }
+       else
+               Assert(read_only_level == 0);
+
+       /*
+        * Start main transaction if we haven't yet; otherwise, change the current
+        * remote (sub)transaction's read/write mode if needed.
+        */
        if (entry->xact_depth <= 0)
        {
-               const char *sql;
+               /*
+                * This is the case when we haven't yet started a main transaction.
+                */
+               StringInfoData sql;
+               bool            ro = (read_only_level == 1);
 
                elog(DEBUG3, "starting remote transaction on connection %p",
                         entry->conn);
 
+               initStringInfo(&sql);
+               appendStringInfoString(&sql, "START TRANSACTION ISOLATION LEVEL ");
                if (IsolationIsSerializable())
-                       sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE";
+                       appendStringInfoString(&sql, "SERIALIZABLE");
                else
-                       sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ";
+                       appendStringInfoString(&sql, "REPEATABLE READ");
+               if (ro)
+                       appendStringInfoString(&sql, " READ ONLY");
+               if (XactDeferrable)
+                       appendStringInfoString(&sql, " DEFERRABLE");
                entry->changing_xact_state = true;
-               do_sql_command(entry->conn, sql);
+               do_sql_command(entry->conn, sql.data);
                entry->xact_depth = 1;
+               if (ro)
+               {
+                       Assert(!entry->xact_read_only);
+                       entry->xact_read_only = true;
+               }
                entry->changing_xact_state = false;
        }
+       else if (!entry->xact_read_only)
+       {
+               /*
+                * The remote (sub)transaction has been opened in read-write mode.
+                */
+               Assert(read_only_level == 0 ||
+                          entry->xact_depth <= read_only_level);
+
+               /*
+                * If its nesting depth matches read_only_level, it means that the
+                * local read-write (sub)transaction that started it has changed to
+                * read-only after that; in which case change it to read-only as well.
+                * Otherwise, the local (sub)transaction is still read-write, so there
+                * is no need to do anything.
+                */
+               if (entry->xact_depth == read_only_level)
+               {
+                       entry->changing_xact_state = true;
+                       do_sql_command(entry->conn, "SET transaction_read_only = on");
+                       entry->xact_read_only = true;
+                       entry->changing_xact_state = false;
+               }
+       }
+       else
+       {
+               /*
+                * The remote (sub)transaction has been opened in read-only mode.
+                */
+               Assert(read_only_level > 0 &&
+                          entry->xact_depth >= read_only_level);
+
+               /*
+                * The local read-only (sub)transaction that started it is guaranteed
+                * to be still read-only (see check_transaction_read_only), so there
+                * is no need to do anything.
+                */
+       }
 
        /*
         * If we're in a subtransaction, stack up savepoints to match our level.
@@ -902,12 +987,21 @@ begin_remote_xact(ConnCacheEntry *entry)
         */
        while (entry->xact_depth < curlevel)
        {
-               char            sql[64];
+               StringInfoData sql;
+               bool            ro = (entry->xact_depth + 1 == read_only_level);
 
-               snprintf(sql, sizeof(sql), "SAVEPOINT s%d", entry->xact_depth + 1);
+               initStringInfo(&sql);
+               appendStringInfo(&sql, "SAVEPOINT s%d", entry->xact_depth + 1);
+               if (ro)
+                       appendStringInfoString(&sql, "; SET transaction_read_only = on");
                entry->changing_xact_state = true;
-               do_sql_command(entry->conn, sql);
+               do_sql_command(entry->conn, sql.data);
                entry->xact_depth++;
+               if (ro)
+               {
+                       Assert(!entry->xact_read_only);
+                       entry->xact_read_only = true;
+               }
                entry->changing_xact_state = false;
        }
 }
@@ -1212,6 +1306,9 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 
        /* Also reset cursor numbering for next transaction */
        cursor_number = 0;
+
+       /* Likewise for read_only_level */
+       read_only_level = 0;
 }
 
 /*
@@ -1310,6 +1407,10 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid,
                                                                           false);
                }
        }
+
+       /* If in read_only_level, reset it */
+       if (curlevel == read_only_level)
+               read_only_level = 0;
 }
 
 /*
@@ -1412,6 +1513,9 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel)
                /* Reset state to show we're out of a transaction */
                entry->xact_depth = 0;
 
+               /* Reset xact r/o state */
+               entry->xact_read_only = false;
+
                /*
                 * If the connection isn't in a good idle state, it is marked as
                 * invalid or keep_connections option of its server is disabled, then
@@ -1432,6 +1536,10 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel)
        {
                /* Reset state to show we're out of a subtransaction */
                entry->xact_depth--;
+
+               /* If in read_only_level, reset xact r/o state */
+               if (entry->xact_depth + 1 == read_only_level)
+                       entry->xact_read_only = false;
        }
 }
 
index ac34a1acacb62913c7d8831b64aad33059a174b8..cd22553236f0578c74924f112b2dff3f96c1d8df 100644 (file)
@@ -12575,6 +12575,142 @@ SELECT count(*) FROM remote_application_name
 DROP FOREIGN TABLE remote_application_name;
 DROP VIEW my_application_name;
 -- ===================================================================
+-- test read-only and/or deferrable transactions
+-- ===================================================================
+CREATE TABLE loct (f1 int, f2 text);
+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;
+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
+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 TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should fail
+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;
+START TRANSACTION;
+SAVEPOINT s;
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+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 TO s;
+RELEASE SAVEPOINT s;
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+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;
+START TRANSACTION;
+SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+ f1 |   f2   
+----+--------
+  1 | foofoo
+  2 | barbar
+(2 rows)
+
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+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 TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+ f1 |   f2   
+----+--------
+  1 | foofoo
+  2 | barbar
+(2 rows)
+
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+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;
+-- Exercise abort code paths in pgfdw_xact_callback/pgfdw_subxact_callback
+-- in situations where multiple connections are involved
+START TRANSACTION;
+SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+ f1 |   f2   
+----+--------
+  1 | foofoo
+  2 | barbar
+(2 rows)
+
+SET transaction_read_only = on;
+SELECT * FROM remt2;  -- should fail
+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 TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+ f1 |   f2   
+----+--------
+  1 | foofoo
+  2 | barbar
+(2 rows)
+
+SET transaction_read_only = on;
+SELECT * FROM remt2;  -- should fail
+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;
+DROP FOREIGN TABLE remt;
+CREATE FOREIGN TABLE remt (f1 int, f2 text)
+  SERVER loopback OPTIONS (table_name 'loct');
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY;
+SELECT * FROM remt;
+ f1 | f2  
+----+-----
+  1 | foo
+  2 | bar
+(2 rows)
+
+COMMIT;
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE DEFERRABLE;
+SELECT * FROM remt;
+ f1 | f2  
+----+-----
+  1 | foo
+  2 | bar
+(2 rows)
+
+COMMIT;
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
+SELECT * FROM remt;
+ f1 | f2  
+----+-----
+  1 | foo
+  2 | bar
+(2 rows)
+
+COMMIT;
+-- Clean up
+DROP FOREIGN TABLE remt;
+DROP FOREIGN TABLE remt2;
+DROP VIEW locv;
+DROP FUNCTION locf();
+DROP TABLE loct;
+-- ===================================================================
 -- test parallel commit and parallel abort
 -- ===================================================================
 ALTER SERVER loopback OPTIONS (ADD parallel_commit 'true');
index 0e218b29a29ed50d63eb09425bbbe5890f828cba..59963e298b846fa40d73b8a85590b5a69966b88f 100644 (file)
@@ -4328,6 +4328,86 @@ SELECT count(*) FROM remote_application_name
 DROP FOREIGN TABLE remote_application_name;
 DROP VIEW my_application_name;
 
+-- ===================================================================
+-- test read-only and/or deferrable transactions
+-- ===================================================================
+CREATE TABLE loct (f1 int, f2 text);
+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;
+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
+ROLLBACK TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should fail
+ROLLBACK;
+
+START TRANSACTION;
+SAVEPOINT s;
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+ROLLBACK TO s;
+RELEASE SAVEPOINT s;
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+ROLLBACK;
+
+START TRANSACTION;
+SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+ROLLBACK TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+SET transaction_read_only = on;
+SELECT * FROM remt;  -- should fail
+ROLLBACK;
+
+-- Exercise abort code paths in pgfdw_xact_callback/pgfdw_subxact_callback
+-- in situations where multiple connections are involved
+START TRANSACTION;
+SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+SET transaction_read_only = on;
+SELECT * FROM remt2;  -- should fail
+ROLLBACK TO s;
+RELEASE SAVEPOINT s;
+SELECT * FROM remt;  -- should work
+SET transaction_read_only = on;
+SELECT * FROM remt2;  -- should fail
+ROLLBACK;
+
+DROP FOREIGN TABLE remt;
+CREATE FOREIGN TABLE remt (f1 int, f2 text)
+  SERVER loopback OPTIONS (table_name 'loct');
+
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY;
+SELECT * FROM remt;
+COMMIT;
+
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE DEFERRABLE;
+SELECT * FROM remt;
+COMMIT;
+
+START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
+SELECT * FROM remt;
+COMMIT;
+
+-- Clean up
+DROP FOREIGN TABLE remt;
+DROP FOREIGN TABLE remt2;
+DROP VIEW locv;
+DROP FUNCTION locf();
+DROP TABLE loct;
+
 -- ===================================================================
 -- test parallel commit and parallel abort
 -- ===================================================================
index de69ddcdebcc7a25208c130878859dd0687b92af..9185c76f9329034ebf11c598a42d67d1de98f242 100644 (file)
@@ -1103,6 +1103,23 @@ CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testp
    <productname>PostgreSQL</productname> release might modify these rules.
   </para>
 
+  <para>
+   The remote transaction is opened in the same read/write mode as the local
+   transaction: if the local transaction is <literal>READ ONLY</literal>,
+   the remote transaction is opened in <literal>READ ONLY</literal> mode,
+   otherwise it is opened in <literal>READ WRITE</literal> mode.
+   (This rule is also applied to remote and local subtransactions.)
+   Note that this does not prevent login triggers executed on the remote
+   server from writing.
+  </para>
+
+  <para>
+   The remote transaction is also opened in the same deferrable mode as the
+   local transaction: if the local transaction is <literal>DEFERRABLE</literal>,
+   the remote transaction is opened in <literal>DEFERRABLE</literal> mode,
+   otherwise it is opened in <literal>NOT DEFERRABLE</literal> mode.
+  </para>
+
   <para>
    Note that it is currently not supported by
    <filename>postgres_fdw</filename> to prepare the remote transaction for
index aafc53e016467507590c61837ced3579654e8fd5..48bc90c967353470854c2101d247dd9ee449b6a8 100644 (file)
@@ -1046,6 +1046,34 @@ TransactionStartedDuringRecovery(void)
        return CurrentTransactionState->startedInRecovery;
 }
 
+/*
+ *     GetTopReadOnlyTransactionNestLevel
+ *
+ * Note: this will return zero when not inside any transaction or when neither
+ * a top-level transaction nor subtransactions are read-only, one when the
+ * top-level transaction is read-only, two when one level of subtransaction is
+ * read-only, etc.
+ *
+ * Note: subtransactions of the topmost read-only transaction are also
+ * read-only, because they inherit read-only mode from the transaction, and
+ * thus can't change to read-write mode (see check_transaction_read_only).
+ */
+int
+GetTopReadOnlyTransactionNestLevel(void)
+{
+       TransactionState s = CurrentTransactionState;
+
+       if (!XactReadOnly)
+               return 0;
+       while (s->nestingLevel > 1)
+       {
+               if (!s->prevXactReadOnly)
+                       return s->nestingLevel;
+               s = s->parent;
+       }
+       return s->nestingLevel;
+}
+
 /*
  *     EnterParallelMode
  */
index f0b4d795071afb5c127d263b6deed52b064029c1..a8cbdf247c86618fda05222abc6e69d0ff5d4b59 100644 (file)
@@ -459,6 +459,7 @@ extern TimestampTz GetCurrentTransactionStopTimestamp(void);
 extern void SetCurrentStatementStartTimestamp(void);
 extern int     GetCurrentTransactionNestLevel(void);
 extern bool TransactionIdIsCurrentTransactionId(TransactionId xid);
+extern int     GetTopReadOnlyTransactionNestLevel(void);
 extern void CommandCounterIncrement(void);
 extern void ForceSyncCommit(void);
 extern void StartTransactionCommand(void);