From 2b67f25aaa6a80664acc1fc3626e919e3a591139 Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Mon, 25 Aug 2025 19:23:50 +0530
Subject: [PATCH v20260724 3/7] Add a view to read contents of shared buffer
 lookup table

The view exposes the contents of the shared buffer lookup table for
debugging, testing and investigation.

This helped me in debugging issues where the buffer descriptor array and
buffer lookup table were out of sync; either the buffer lookup table had
a mapping page->buffer which wasn't present in the buffer descriptor
array or a page in the buffer descriptor array didn't have corresponding
entry in the buffer lookup table. pg_buffercache doesn't help with those
kind of issues. Also doing that under the debugger in very painful.

I intend to keep this patch while the rest of the code matures. If it is
found useful as a debugging tool, we may consider make it committable
and commit it.

Author: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
---
 .../expected/pg_buffercache.out               | 39 ++++++++
 .../pg_buffercache--1.5--1.6.sql              | 24 +++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 17 ++++
 contrib/pg_buffercache/sql/pg_buffercache.sql | 20 +++++
 doc/src/sgml/system-views.sgml                | 89 +++++++++++++++++++
 src/backend/storage/buffer/buf_table.c        | 58 ++++++++++++
 src/include/storage/buf_internals.h           |  2 +
 7 files changed, 249 insertions(+)

diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index c52a8491ff9..1ce5a970334 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -33,6 +33,26 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts() WHERE buffers >= 0;
  t
 (1 row)
 
+-- Test the buffer lookup table function and count is <= shared_buffers
+select count(*) <= (select setting::bigint
+                    from pg_settings
+                    where name = 'shared_buffers')
+from pg_buffercache_lookup_table_entries();
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Check that pg_buffercache_lookup_table view works and count is <= shared_buffers
+select count(*) <= (select setting::bigint
+                    from pg_settings
+                    where name = 'shared_buffers')
+from pg_buffercache_lookup_table;
+ ?column? 
+----------
+ t
+(1 row)
+
 -- Check that the functions / views can't be accessed by default. To avoid
 -- having to create a dedicated user, use the pg_database_owner pseudo-role.
 SET ROLE pg_database_owner;
@@ -46,6 +66,10 @@ SELECT * FROM pg_buffercache_summary();
 ERROR:  permission denied for function pg_buffercache_summary
 SELECT * FROM pg_buffercache_usage_counts();
 ERROR:  permission denied for function pg_buffercache_usage_counts
+SELECT * FROM pg_buffercache_lookup_table_entries();
+ERROR:  permission denied for function pg_buffercache_lookup_table_entries
+SELECT * FROM pg_buffercache_lookup_table;
+ERROR:  permission denied for view pg_buffercache_lookup_table
 RESET role;
 -- Check that pg_monitor is allowed to query view / function
 SET ROLE pg_monitor;
@@ -81,6 +105,21 @@ FROM pg_buffercache_pages() AS p
 LIMIT 1;
 ERROR:  function return row and query-specified return row do not match
 DETAIL:  Returned type boolean at ordinal position 7, but query expects text.
+RESET role;
+-- Check that pg_read_all_stats is allowed to query buffer lookup table
+SET ROLE pg_read_all_stats;
+SELECT count(*) >= 0 FROM pg_buffercache_lookup_table_entries();
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT count(*) >= 0 FROM pg_buffercache_lookup_table;
+ ?column? 
+----------
+ t
+(1 row)
+
 RESET role;
 ------
 ---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 458f054a691..9bf58567878 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -44,3 +44,27 @@ CREATE FUNCTION pg_buffercache_evict_all(
     OUT buffers_skipped int4)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+-- Add the buffer lookup table function
+CREATE FUNCTION pg_buffercache_lookup_table_entries(
+    OUT tablespace oid,
+    OUT database oid,
+    OUT relfilenode oid,
+    OUT forknum int2,
+    OUT blocknum int8,
+    OUT bufferid int4)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_buffercache_lookup_table_entries'
+LANGUAGE C PARALLEL SAFE VOLATILE;
+
+-- Create a view for convenient access.
+CREATE VIEW pg_buffercache_lookup_table AS
+    SELECT * FROM pg_buffercache_lookup_table_entries();
+
+-- Don't want these to be available to public.
+REVOKE ALL ON FUNCTION pg_buffercache_lookup_table_entries() FROM PUBLIC;
+REVOKE ALL ON pg_buffercache_lookup_table FROM PUBLIC;
+
+-- Grant access to monitoring role.
+GRANT EXECUTE ON FUNCTION pg_buffercache_lookup_table_entries() TO pg_read_all_stats;
+GRANT SELECT ON pg_buffercache_lookup_table TO pg_read_all_stats;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 510455998aa..9512f1efa2f 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -77,6 +77,7 @@ PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
 PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_lookup_table_entries);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -922,3 +923,19 @@ pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Return lookup table content as a set of records.
+ */
+Datum
+pg_buffercache_lookup_table_entries(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	BufTableGetContents(rsinfo->setResult, rsinfo->setDesc);
+
+	return (Datum) 0;
+}
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index be89b5f5a3a..d0726b2b2f9 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -18,6 +18,18 @@ from pg_buffercache_summary();
 
 SELECT count(*) > 0 FROM pg_buffercache_usage_counts() WHERE buffers >= 0;
 
+-- Test the buffer lookup table function and count is <= shared_buffers
+select count(*) <= (select setting::bigint
+                    from pg_settings
+                    where name = 'shared_buffers')
+from pg_buffercache_lookup_table_entries();
+
+-- Check that pg_buffercache_lookup_table view works and count is <= shared_buffers
+select count(*) <= (select setting::bigint
+                    from pg_settings
+                    where name = 'shared_buffers')
+from pg_buffercache_lookup_table;
+
 -- Check that the functions / views can't be accessed by default. To avoid
 -- having to create a dedicated user, use the pg_database_owner pseudo-role.
 SET ROLE pg_database_owner;
@@ -26,6 +38,8 @@ SELECT * FROM pg_buffercache_os_pages;
 SELECT * FROM pg_buffercache_pages() AS p (wrong int);
 SELECT * FROM pg_buffercache_summary();
 SELECT * FROM pg_buffercache_usage_counts();
+SELECT * FROM pg_buffercache_lookup_table_entries();
+SELECT * FROM pg_buffercache_lookup_table;
 RESET role;
 
 -- Check that pg_monitor is allowed to query view / function
@@ -42,6 +56,12 @@ FROM pg_buffercache_pages() AS p
 LIMIT 1;
 RESET role;
 
+-- Check that pg_read_all_stats is allowed to query buffer lookup table
+SET ROLE pg_read_all_stats;
+SELECT count(*) >= 0 FROM pg_buffercache_lookup_table_entries();
+SELECT count(*) >= 0 FROM pg_buffercache_lookup_table;
+RESET role;
+
 
 ------
 ---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 5ea19d68622..6b905498337 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -71,6 +71,11 @@
       <entry>backend memory contexts</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-buffer-lookup-table"><structname>pg_buffer_lookup_table</structname></link></entry>
+      <entry>shared buffer lookup table</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-config"><structname>pg_config</structname></link></entry>
       <entry>compile-time configuration parameters</entry>
@@ -929,6 +934,90 @@ AND c1.path[c2.level] = c2.path[c2.level];
   </para>
  </sect1>
 
+ <sect1 id="view-pg-buffer-lookup-table">
+  <title><structname>pg_buffer_lookup_table</structname></title>
+  <indexterm>
+   <primary>pg_buffer_lookup_table</primary>
+  </indexterm>
+  <para>
+   The <structname>pg_buffer_lookup_table</structname> view exposes the current
+   contents of the shared buffer lookup table. Each row represents an entry in
+   the lookup table mapping a relation page to the ID of buffer in which it is
+   cached. The shared buffer lookup table is locked for a short duration while
+   reading so as to ensure consistency. This may affect performance if this view
+   is queried very frequently.
+  </para>
+  <table id="pg-buffer-lookup-table-view" xreflabel="pg_buffer_lookup_table">
+   <title><structname>pg_buffer_lookup_table</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tablespace</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of the tablespace containing the relation
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>database</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of the database containing the relation (zero for shared relations)
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relfilenode</structfield> <type>oid</type>
+      </para>
+      <para>
+       relfilenode identifying the relation
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>forknum</structfield> <type>int2</type>
+      </para>
+      <para>
+       Fork number within the relation (see <xref linkend="storage-file-layout"/>)
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blocknum</structfield> <type>int8</type>
+      </para>
+      <para>
+       Block number within the relation
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>bufferid</structfield> <type>int4</type>
+      </para>
+      <para>
+       ID of the buffer caching the page
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <para>
+   Access to this view is restricted to members of the
+   <literal>pg_read_all_stats</literal> role by default.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-config">
   <title><structname>pg_config</structname></title>
 
diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c
index 5c8ccbee13f..c82b71deaa6 100644
--- a/src/backend/storage/buffer/buf_table.c
+++ b/src/backend/storage/buffer/buf_table.c
@@ -21,7 +21,12 @@
  */
 #include "postgres.h"
 
+#include "fmgr.h"
+#include "funcapi.h"
 #include "storage/buf_internals.h"
+#include "utils/rel.h"
+#include "utils/builtins.h"
+#include "storage/lwlock.h"
 #include "storage/subsystems.h"
 
 /* entry for buffer lookup hashtable */
@@ -167,3 +172,56 @@ BufTableDelete(BufferTag *tagPtr, uint32 hashcode)
 	if (!result)				/* shouldn't happen */
 		elog(ERROR, "shared buffer hash table corrupted");
 }
+
+/*
+ * BufTableGetContents
+ *		Fill the given tuplestore with contents of the shared buffer lookup table
+ *
+ * This function is used by pg_buffercache extension to expose buffer lookup
+ * table contents via SQL. The caller is responsible for setting up the
+ * tuplestore and result set info.
+ */
+void
+BufTableGetContents(Tuplestorestate *tupstore, TupleDesc tupdesc)
+{
+/* Expected number of attributes of the buffer lookup table entry. */
+#define BUFTABLE_CONTENTS_COLS 6
+
+	HASH_SEQ_STATUS hstat;
+	BufferLookupEnt *ent;
+	Datum		values[BUFTABLE_CONTENTS_COLS];
+	bool		nulls[BUFTABLE_CONTENTS_COLS];
+	int			i;
+
+	memset(nulls, 0, sizeof(nulls));
+
+	Assert(tupdesc->natts == BUFTABLE_CONTENTS_COLS);
+
+	/*
+	 * Lock all buffer mapping partitions to ensure a consistent view of the
+	 * hash table during the scan. Must grab LWLocks in partition-number order
+	 * to avoid LWLock deadlock.
+	 */
+	for (i = 0; i < NUM_BUFFER_PARTITIONS; i++)
+		LWLockAcquire(BufMappingPartitionLockByIndex(i), LW_SHARED);
+
+	hash_seq_init(&hstat, SharedBufHash);
+	while ((ent = (BufferLookupEnt *) hash_seq_search(&hstat)) != NULL)
+	{
+		values[0] = ObjectIdGetDatum(ent->key.spcOid);
+		values[1] = ObjectIdGetDatum(ent->key.dbOid);
+		values[2] = ObjectIdGetDatum(ent->key.relNumber);
+		values[3] = ObjectIdGetDatum(ent->key.forkNum);
+		values[4] = Int64GetDatum(ent->key.blockNum);
+		values[5] = Int32GetDatum(ent->id);
+
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+
+	/*
+	 * Release all buffer mapping partition locks in the reverse order so as
+	 * to avoid LWLock deadlock.
+	 */
+	for (i = NUM_BUFFER_PARTITIONS - 1; i >= 0; i--)
+		LWLockRelease(BufMappingPartitionLockByIndex(i));
+}
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index a7606c6e92b..678065b3de2 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -29,6 +29,7 @@
 #include "storage/spin.h"
 #include "utils/relcache.h"
 #include "utils/resowner.h"
+#include "utils/tuplestore.h"
 
 /*
  * Buffer state is a single 64-bit variable where following data is combined.
@@ -599,6 +600,7 @@ extern uint32 BufTableHashCode(BufferTag *tagPtr);
 extern int	BufTableLookup(BufferTag *tagPtr, uint32 hashcode);
 extern int	BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id);
 extern void BufTableDelete(BufferTag *tagPtr, uint32 hashcode);
+extern void BufTableGetContents(Tuplestorestate *tupstore, TupleDesc tupdesc);
 
 /* localbuf.c */
 extern bool PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount);
-- 
2.34.1

