postgres_fdw: Inherit the local transaction's access/deferrable modes.
authorEtsuro Fujita <efujita@postgresql.org>
Sun, 1 Jun 2025 08:30:00 +0000 (17:30 +0900)
committerEtsuro Fujita <efujita@postgresql.org>
Sun, 1 Jun 2025 08:30:00 +0000 (17:30 +0900)
Previously, postgres_fdw always 1) opened a remote transaction in READ
WRITE mode even when the local transaction was READ ONLY, causing a READ
ONLY transaction using it that references a foreign table mapped to a
remote view executing a volatile function to write in the remote side,
and 2) opened the remote transaction in NOT DEFERRABLE mode even when
the local transaction was DEFERRABLE, causing a SERIALIZABLE READ ONLY
DEFERRABLE transaction using it to abort due to a serialization failure
in the remote side.

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

Although these issues exist since the introduction of postgres_fdw,
there have been no reports from the field.  So it seems fine to just 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>
Discussion: https://postgr.es/m/CAPmGK16n_hcUUWuOdmeUS%2Bw4Q6dZvTEDHb%3DOP%3D5JBzo-M3QmpQ%40mail.gmail.com

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 304f3c20f835697d352dbfb91b1b11d54e6afc0a..caf1446269635b80189e8a31afcd033e47b4f3b0 100644 (file)
@@ -58,6 +58,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 */
@@ -84,6 +85,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 nesting level of the topmost read-only transaction determined
+ * by GetTopReadOnlyTransactionNestLevel()
+ */
+static int top_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;
@@ -372,6 +379,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;
@@ -843,29 +851,81 @@ 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 */
+   /*
+    * Set the nesting level of the topmost read-only transaction if the
+    * current transaction is read-only and we haven't yet.  Once it's set,
+    * it's retained until that transaction is committed/aborted, and then
+    * reset (see pgfdw_xact_callback and pgfdw_subxact_callback).
+    */
+   if (XactReadOnly)
+   {
+       if (top_read_only_level == 0)
+           top_read_only_level = GetTopReadOnlyTransactionNestLevel();
+       Assert(top_read_only_level > 0);
+   }
+   else
+       Assert(top_read_only_level == 0);
+
+   /*
+    * Start main transaction if we haven't yet; otherwise, change the
+    * already-started remote transaction/subtransaction to read-only if the
+    * local transaction/subtransaction have been done so after starting them
+    * and we haven't yet.
+    */
    if (entry->xact_depth <= 0)
    {
-       const char *sql;
+       StringInfoData sql;
+       bool        ro = (top_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)
+   {
+       Assert(top_read_only_level == 0 ||
+              entry->xact_depth <= top_read_only_level);
+       if (entry->xact_depth == top_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
+       Assert(top_read_only_level > 0 &&
+              entry->xact_depth >= top_read_only_level);
 
    /*
     * If we're in a subtransaction, stack up savepoints to match our level.
@@ -874,12 +934,21 @@ begin_remote_xact(ConnCacheEntry *entry)
     */
    while (entry->xact_depth < curlevel)
    {
-       char        sql[64];
+       StringInfoData sql;
+       bool        ro = (entry->xact_depth + 1 == top_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;
    }
 }
@@ -1174,6 +1243,9 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 
    /* Also reset cursor numbering for next transaction */
    cursor_number = 0;
+
+   /* Likewise for top_read_only_level */
+   top_read_only_level = 0;
 }
 
 /*
@@ -1272,6 +1344,10 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid,
                                       false);
        }
    }
+
+   /* If in the topmost read-only transaction, reset top_read_only_level */
+   if (curlevel == top_read_only_level)
+       top_read_only_level = 0;
 }
 
 /*
@@ -1374,6 +1450,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
@@ -1394,6 +1473,10 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel)
    {
        /* Reset state to show we're out of a subtransaction */
        entry->xact_depth--;
+
+       /* If in the topmost read-only transaction, reset xact r/o state */
+       if (entry->xact_depth + 1 == top_read_only_level)
+           entry->xact_read_only = false;
    }
 }
 
index 2185b42bb4f79f976dc6d8ed4936dae3ce468c70..eb4716bed813277cfbc391831ee930a82854c2f8 100644 (file)
@@ -12384,6 +12384,140 @@ 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;
+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 e534b40de3c76e133eefbbc0b898d885310a5c22..20a535b99d82f6177dc45e21c3afdd3500f8401f 100644 (file)
@@ -4200,6 +4200,84 @@ 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;
+
+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 781a01067f7d6d0c8ca15b7aba68f040646d655a..c464716e3ce192cccf4e450e8b25b982d0459efc 100644 (file)
@@ -1077,6 +1077,21 @@ postgres=# SELECT postgres_fdw_disconnect_all();
    <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.)
+  </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 b885513f76541bc165a41ce78f83623703c2410a..2e67e998adbc7b04ca95b5699ba23563c7301d62 100644 (file)
@@ -1044,6 +1044,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 b2bc10ee04196465d4cbb0622892b75108e1bf75..7f11b91979941c1263c78dbeb48d7ba7bfbdade3 100644 (file)
@@ -458,6 +458,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);