summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>2025-08-22 13:07:46 +0300
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>2025-08-22 13:07:46 +0300
commit50f770c3d92cfb2d62724d8c810d37c1f6249a11 (patch)
tree33f905e45b618bab9b4e6f7012cd8eea9e15b122 /src
parent16a0039dc0d1d0cdfadaf38cd3a30f3c8f590c48 (diff)
Revert GetTransactionSnapshot() to return historic snapshot during LR
Commit 1585ff7387 changed GetTransactionSnapshot() to throw an error if it's called during logical decoding, instead of returning the historic snapshot. I made that change for extra protection, because a historic snapshot can only be used to access catalog tables while GetTransactionSnapshot() is usually called when you're executing arbitrary queries. You might get very subtle visibility problems if you tried to use the historic snapshot for arbitrary queries. There's no built-in code in PostgreSQL that calls GetTransactionSnapshot() during logical decoding, but it turns out that the pglogical extension does just that, to evaluate row filter expressions. You would get weird results if the row filter runs arbitrary queries, but it is sane as long as you don't access any non-catalog tables. Even though there are no checks to enforce that in pglogical, a typical row filter expression does not access any tables and works fine. Accessing tables marked with the user_catalog_table = true option is also OK. To fix pglogical with row filters, and any other extensions that might do similar things, revert GetTransactionSnapshot() to return a historic snapshot during logical decoding. To try to still catch the unsafe usage of historic snapshots, add checks in heap_beginscan() and index_beginscan() to complain if you try to use a historic snapshot to scan a non-catalog table. We're very close to the version 18 release however, so add those new checks only in master. Backpatch-through: 18 Reported-by: Noah Misch <noah@leadboat.com> Reviewed-by: Noah Misch <noah@leadboat.com> Discussion: https://www.postgresql.org/message-id/20250809222338.cc.nmisch@google.com
Diffstat (limited to 'src')
-rw-r--r--src/backend/access/heap/heapam.c9
-rw-r--r--src/backend/access/index/indexam.c8
-rw-r--r--src/backend/utils/time/snapmgr.c19
-rw-r--r--src/include/utils/snapmgr.h3
4 files changed, 35 insertions, 4 deletions
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 0dcd6ee817e..ee692c03c3c 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1143,6 +1143,15 @@ heap_beginscan(Relation relation, Snapshot snapshot,
if (!(snapshot && IsMVCCSnapshot(snapshot)))
scan->rs_base.rs_flags &= ~SO_ALLOW_PAGEMODE;
+ /* Check that a historic snapshot is not used for non-catalog tables */
+ if (snapshot &&
+ IsHistoricMVCCSnapshot(snapshot) &&
+ !RelationIsAccessibleInLogicalDecoding(relation))
+ {
+ elog(ERROR, "cannot query non-catalog table \"%s\" during logical decoding",
+ RelationGetRelationName(relation));
+ }
+
/*
* For seqscan and sample scans in a serializable transaction, acquire a
* predicate lock on the entire relation. This is required not only to
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1a4f36fe0a9..31b22d9f397 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -263,6 +263,14 @@ index_beginscan(Relation heapRelation,
Assert(snapshot != InvalidSnapshot);
+ /* Check that a historic snapshot is not used for non-catalog tables */
+ if (IsHistoricMVCCSnapshot(snapshot) &&
+ !RelationIsAccessibleInLogicalDecoding(heapRelation))
+ {
+ elog(ERROR, "cannot query non-catalog table \"%s\" during logical decoding",
+ RelationGetRelationName(heapRelation));
+ }
+
scan = index_beginscan_internal(indexRelation, nkeys, norderbys, snapshot, NULL, false);
/*
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index ea35f30f494..65561cc6bc3 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -271,12 +271,23 @@ Snapshot
GetTransactionSnapshot(void)
{
/*
- * This should not be called while doing logical decoding. Historic
- * snapshots are only usable for catalog access, not for general-purpose
- * queries.
+ * Return historic snapshot if doing logical decoding.
+ *
+ * Historic snapshots are only usable for catalog access, not for
+ * general-purpose queries. The caller is responsible for ensuring that
+ * the snapshot is used correctly! (PostgreSQL code never calls this
+ * during logical decoding, but extensions can do it.)
*/
if (HistoricSnapshotActive())
- elog(ERROR, "cannot take query snapshot during logical decoding");
+ {
+ /*
+ * We'll never need a non-historic transaction snapshot in this
+ * (sub-)transaction, so there's no need to be careful to set one up
+ * for later calls to GetTransactionSnapshot().
+ */
+ Assert(!FirstSnapshotSet);
+ return HistoricSnapshot;
+ }
/* First call in transaction? */
if (!FirstSnapshotSet)
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index d346be71642..604c1f90216 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -56,6 +56,9 @@ extern PGDLLIMPORT SnapshotData SnapshotToastData;
((snapshot)->snapshot_type == SNAPSHOT_MVCC || \
(snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+#define IsHistoricMVCCSnapshot(snapshot) \
+ ((snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+
extern Snapshot GetTransactionSnapshot(void);
extern Snapshot GetLatestSnapshot(void);
extern void SnapshotSetCommandId(CommandId curcid);