]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
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);