From 11068d11afd02e62acaece1fae7ce4d9a4fce743 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
Date: Fri, 6 Nov 2026 14:28:09 +0100
Subject: [PATCH v5 2/2] pgstattuple: Reject unlogged relations during recovery

Unlogged tables and indexes have no data on a standby, so reading one
there must fail.  pgstattuple(), pgstattuple_approx(), pg_relpages(),
pgstatindex(), pgstatginindex() and pgstathashindex() read the relation
directly instead of going through the planner, so on a standby they
failed with a confusing low-level message instead of the error that
SELECT reports:

  ERROR:  could not open file "base/5/16384": No such file or directory

Check for this case and report the same error as SELECT:

  ERROR:  cannot access temporary or unlogged relations during recovery

This check and the existing check against temporary relations of other
sessions are now grouped in a new routine, check_relation_accessible(),
used by all these functions except pg_relpages().  pg_relpages() keeps
only the recovery check, as it works on temporary tables of other
sessions.

The error for temporary relations of other sessions now says "indexes"
or "tables" depending on the relation kind.  Previously pgstatindex()
and pgstattuple() said "tables" even for an index, while
pgstatginindex() and pgstathashindex() said "indexes".

Author: Jim Jones <jim.jones@uni-muenster.de>
Reported-by: Zsolt Parragi <zsolt.parragi@percona.com>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Surya Poondla <suryapoondla4@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/82ba0876-6127-4770-b900-aa1bd52abfed@uni-muenster.de
---
 contrib/pgstattuple/pgstatapprox.c | 11 ++------
 contrib/pgstattuple/pgstatindex.c  | 42 ++++++++++--------------------
 contrib/pgstattuple/pgstattuple.c  | 38 ++++++++++++++++++++++-----
 contrib/pgstattuple/pgstattuple.h  | 21 +++++++++++++++
 4 files changed, 69 insertions(+), 43 deletions(-)
 create mode 100644 contrib/pgstattuple/pgstattuple.h

diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..32dcc50586f 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -20,6 +20,7 @@
 #include "commands/vacuum.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/freespace.h"
 #include "storage/procarray.h"
@@ -322,15 +323,7 @@ pgstattuple_approx_internal(Oid relid, FunctionCallInfo fcinfo)
 
 	rel = relation_open(relid, AccessShareLock);
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel);
 
 	/*
 	 * We support only relation kinds with a visibility map and a free space
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..56e2e09c1f2 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -32,10 +32,12 @@
 #include "access/htup_details.h"
 #include "access/nbtree.h"
 #include "access/relation.h"
+#include "access/xlog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/read_stream.h"
 #include "utils/rel.h"
@@ -228,15 +230,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 				 errmsg("relation \"%s\" is not a btree index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+	check_relation_accessible(rel);
 
 	/*
 	 * A !indisready index could lead to ERRCODE_DATA_CORRUPTED later, so exit
@@ -482,7 +476,15 @@ pg_relpages_impl(Relation rel)
 						RelationGetRelationName(rel)),
 				 errdetail_relkind_not_supported(rel->rd_rel->relkind)));
 
-	/* note: this will work OK on non-local temp tables */
+	/*
+	 * Temporary tables of other sessions are not rejected, as only the
+	 * relation size is needed, but temporary and unlogged relations are
+	 * inaccessible during recovery.
+	 */
+	if (!RelationIsPermanent(rel) && RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot access temporary or unlogged relations during recovery")));
 
 	relpages = RelationGetNumberOfBlocks(rel);
 
@@ -547,15 +549,7 @@ pgstatginindex_internal(Oid relid, FunctionCallInfo fcinfo)
 				 errmsg("relation \"%s\" is not a GIN index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary indexes of other sessions")));
+	check_relation_accessible(rel);
 
 	/* see pgstatindex_impl */
 	if (!rel->rd_index->indisvalid)
@@ -637,15 +631,7 @@ pgstathashindex(PG_FUNCTION_ARGS)
 				 errmsg("relation \"%s\" is not a hash index",
 						RelationGetRelationName(rel))));
 
-	/*
-	 * Reject attempts to read non-local temporary relations; we would be
-	 * likely to get wrong data since we have no visibility into the owning
-	 * session's local buffers.
-	 */
-	if (RELATION_IS_OTHER_TEMP(rel))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary indexes of other sessions")));
+	check_relation_accessible(rel);
 
 	/* see pgstatindex_impl */
 	if (!rel->rd_index->indisvalid)
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..2a3ce1b32b2 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -30,10 +30,12 @@
 #include "access/nbtree.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
+#include "access/xlog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am_d.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "pgstattuple.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "utils/varlena.h"
@@ -237,22 +239,46 @@ pgstattuplebyid_v1_5(PG_FUNCTION_ARGS)
 }
 
 /*
- * pgstat_relation
+ * check_relation_accessible
+ *
+ * Check that a relation can be read by this session.
  */
-static Datum
-pgstat_relation(Relation rel, FunctionCallInfo fcinfo)
+void
+check_relation_accessible(Relation rel)
 {
-	const char *err;
-
 	/*
 	 * Reject attempts to read non-local temporary relations; we would be
 	 * likely to get wrong data since we have no visibility into the owning
 	 * session's local buffers.
 	 */
 	if (RELATION_IS_OTHER_TEMP(rel))
+	{
+		if (rel->rd_rel->relkind == RELKIND_INDEX)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot access temporary indexes of other sessions")));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot access temporary tables of other sessions")));
+	}
+
+	/* Temporary and unlogged relations are inaccessible during recovery. */
+	if (!RelationIsPermanent(rel) && RecoveryInProgress())
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot access temporary tables of other sessions")));
+				 errmsg("cannot access temporary or unlogged relations during recovery")));
+}
+
+/*
+ * pgstat_relation
+ */
+static Datum
+pgstat_relation(Relation rel, FunctionCallInfo fcinfo)
+{
+	const char *err;
+
+	check_relation_accessible(rel);
 
 	if (RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind) ||
 		rel->rd_rel->relkind == RELKIND_SEQUENCE)
diff --git a/contrib/pgstattuple/pgstattuple.h b/contrib/pgstattuple/pgstattuple.h
new file mode 100644
index 00000000000..1617b23f901
--- /dev/null
+++ b/contrib/pgstattuple/pgstattuple.h
@@ -0,0 +1,21 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgstattuple.h
+ *	  Common functions for pgstattuple.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  contrib/pgstattuple/pgstattuple.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _PGSTATTUPLE_H_
+#define _PGSTATTUPLE_H_
+
+#include "utils/relcache.h"
+
+/* in pgstattuple.c */
+extern void check_relation_accessible(Relation rel);
+
+#endif							/* _PGSTATTUPLE_H_ */
-- 
2.56.0

