From 5b61cea77f6747c2fb94650a41821e68d94f50ef Mon Sep 17 00:00:00 2001
From: Georgy Shelkovy <rekgrpth@gmail.com>
Date: Tue, 27 Oct 2026 19:34:22 +0500
Subject: [PATCH] Add new_oid_hook

An extension may propose the OID of a new catalog row, e.g. to give an
object the same OID on several servers.  The server checks a proposed
OID as any other candidate: one in use, or in the range of system
objects, is an error rather than a reason to try another.  TOAST value
OIDs are not proposed, nor OIDs in bootstrap mode or during pg_upgrade.

A proposed relation OID that names an existing file is kept, and the
relation takes its relfilenumber from a call of its own, as
NewOidNeedsRelFileNumber() tells heap_create_with_catalog() and
index_create().

NEW_OID_HOOK_VERSION tells extensions that the hook exists.  The new
test module test_new_oid_hook exercises it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
 src/backend/catalog/catalog.c                 | 114 +++++++++++++++---
 src/backend/catalog/heap.c                    |   7 ++
 src/backend/catalog/index.c                   |   6 +
 src/include/catalog/catalog.h                 |  12 ++
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_new_oid_hook/Makefile   |  20 +++
 src/test/modules/test_new_oid_hook/README     |   9 ++
 .../expected/test_new_oid_hook.out            | 102 ++++++++++++++++
 .../modules/test_new_oid_hook/meson.build     |  33 +++++
 .../sql/test_new_oid_hook.sql                 |  47 ++++++++
 .../test_new_oid_hook--1.0.sql                |   8 ++
 .../test_new_oid_hook/test_new_oid_hook.c     |  61 ++++++++++
 .../test_new_oid_hook.control                 |   4 +
 src/tools/pgindent/typedefs.list              |   1 +
 15 files changed, 412 insertions(+), 14 deletions(-)
 create mode 100644 src/test/modules/test_new_oid_hook/Makefile
 create mode 100644 src/test/modules/test_new_oid_hook/README
 create mode 100644 src/test/modules/test_new_oid_hook/expected/test_new_oid_hook.out
 create mode 100644 src/test/modules/test_new_oid_hook/meson.build
 create mode 100644 src/test/modules/test_new_oid_hook/sql/test_new_oid_hook.sql
 create mode 100644 src/test/modules/test_new_oid_hook/test_new_oid_hook--1.0.sql
 create mode 100644 src/test/modules/test_new_oid_hook/test_new_oid_hook.c
 create mode 100644 src/test/modules/test_new_oid_hook/test_new_oid_hook.control

diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index cf9b88b3e25..ae217f7045e 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -55,6 +55,18 @@
 #define GETNEWOID_LOG_THRESHOLD 1000000
 #define GETNEWOID_LOG_MAX_INTERVAL 128000000
 
+/* Hook for an extension to propose the OID of a new catalog row */
+new_oid_hook_type new_oid_hook = NULL;
+
+/* Did new_oid_hook propose the OID that GetNewOidWithIndex() returned last? */
+static bool last_oid_proposed = false;
+
+/* See NewOidNeedsRelFileNumber() */
+static bool new_oid_needs_relfilenumber = false;
+
+static bool OidInUse(Relation relation, Oid indexId, AttrNumber oidcolumn,
+					 Oid oid);
+
 /*
  * IsSystemRelation
  *		True iff the relation is either a system catalog or a toast table.
@@ -469,14 +481,19 @@ IsPinnedObject(Oid classId, Oid objectId)
  * consecutive existing OIDs.  This is a mostly reasonable assumption for
  * system catalogs.
  *
+ * An extension may propose the OID of a catalog row through new_oid_hook,
+ * e.g. to give an object the same OID on several servers.  TOAST value OIDs
+ * are not proposed, and neither are OIDs in bootstrap mode or during
+ * pg_upgrade.  A proposed OID is checked like any other candidate, but one
+ * in use or in the range of system objects is an error rather than a reason
+ * to try another: the extension would accept no other.
+ *
  * Caller must have a suitable lock on the relation.
  */
 Oid
 GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
 {
 	Oid			newOid;
-	SysScanDesc scan;
-	ScanKeyData key;
 	bool		collides;
 	uint64		retries = 0;
 	uint64		retries_before_log = GETNEWOID_LOG_THRESHOLD;
@@ -484,6 +501,8 @@ GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
 	/* Only system relations are supported */
 	Assert(IsSystemRelation(relation));
 
+	last_oid_proposed = false;
+
 	/* In bootstrap mode, we don't have any indexes to use */
 	if (IsBootstrapProcessingMode())
 		return GetNewObjectId();
@@ -496,6 +515,26 @@ GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
 	 */
 	Assert(!IsBinaryUpgrade || RelationGetRelid(relation) != TypeRelationId);
 
+	/* Let an extension propose the OID of a catalog row */
+	if (unlikely(new_oid_hook != NULL) && !IsBinaryUpgrade &&
+		IsCatalogRelation(relation) && !IsToastRelation(relation))
+	{
+		newOid = new_oid_hook(relation, indexId, oidcolumn);
+
+		if (OidIsValid(newOid))
+		{
+			if (newOid < FirstNormalObjectId)
+				elog(ERROR, "proposed OID %u for relation \"%s\" is in the range of system objects",
+					 newOid, RelationGetRelationName(relation));
+			if (OidInUse(relation, indexId, oidcolumn, newOid))
+				elog(ERROR, "proposed OID %u is already in use in relation \"%s\"",
+					 newOid, RelationGetRelationName(relation));
+
+			last_oid_proposed = true;
+			return newOid;
+		}
+	}
+
 	/* Generate new OIDs until we find one not in the table */
 	do
 	{
@@ -503,18 +542,7 @@ GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
 
 		newOid = GetNewObjectId();
 
-		ScanKeyInit(&key,
-					oidcolumn,
-					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(newOid));
-
-		/* see notes above about using SnapshotAny */
-		scan = systable_beginscan(relation, indexId, true,
-								  SnapshotAny, 1, &key);
-
-		collides = HeapTupleIsValid(systable_getnext(scan));
-
-		systable_endscan(scan);
+		collides = OidInUse(relation, indexId, oidcolumn, newOid);
 
 		/*
 		 * Log that we iterate more than GETNEWOID_LOG_THRESHOLD but have not
@@ -564,6 +592,33 @@ GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
 	return newOid;
 }
 
+/*
+ * OidInUse
+ *		Is the OID in the relation, as GetNewOidWithIndex() sees it?
+ */
+static bool
+OidInUse(Relation relation, Oid indexId, AttrNumber oidcolumn, Oid oid)
+{
+	SysScanDesc scan;
+	ScanKeyData key;
+	bool		found;
+
+	ScanKeyInit(&key,
+				oidcolumn,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(oid));
+
+	/* see notes on GetNewOidWithIndex() about using SnapshotAny */
+	scan = systable_beginscan(relation, indexId, true,
+							  SnapshotAny, 1, &key);
+
+	found = HeapTupleIsValid(systable_getnext(scan));
+
+	systable_endscan(scan);
+
+	return found;
+}
+
 /*
  * GetNewRelFileNumber
  *		Generate a new relfilenumber that is unique within the
@@ -574,6 +629,10 @@ GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn)
  * is also an unused OID within pg_class.  If the result is to be used only
  * as a relfilenumber for an existing relation, pass NULL for pg_class.
  *
+ * An OID that new_oid_hook proposed is returned even if a file of that
+ * number exists; NewOidNeedsRelFileNumber() then tells the caller to take
+ * the relfilenumber from a second call with a NULL pg_class.
+ *
  * As with GetNewOidWithIndex(), there is some theoretical risk of a race
  * condition, but it doesn't seem worth worrying about.
  *
@@ -622,6 +681,8 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
 	 */
 	rlocator.backend = procNumber;
 
+	new_oid_needs_relfilenumber = false;
+
 	do
 	{
 		CHECK_FOR_INTERRUPTS();
@@ -640,6 +701,17 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
 		{
 			/* definite collision */
 			collides = true;
+
+			/*
+			 * Keep an OID that new_oid_hook proposed, as there is no other
+			 * the extension would accept, and leave the relfilenumber to the
+			 * caller.
+			 */
+			if (pg_class && last_oid_proposed)
+			{
+				collides = false;
+				new_oid_needs_relfilenumber = true;
+			}
 		}
 		else
 		{
@@ -657,6 +729,20 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
 	return rlocator.locator.relNumber;
 }
 
+/*
+ * NewOidNeedsRelFileNumber
+ *		Must the relation whose OID GetNewRelFileNumber() returned last take
+ *		its relfilenumber from another call?
+ *
+ * That is the case only for an OID that new_oid_hook proposed and that
+ * names an existing file already.
+ */
+bool
+NewOidNeedsRelFileNumber(void)
+{
+	return new_oid_needs_relfilenumber;
+}
+
 /*
  * SQL callable interface for GetNewOidWithIndex().  Outside of initdb's
  * direct insertions into catalog tables, and recovering from corruption, this
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index ce3d3466126..7418a61a0f6 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1282,8 +1282,15 @@ heap_create_with_catalog(const char *relname,
 		}
 
 		if (!OidIsValid(relid))
+		{
 			relid = GetNewRelFileNumber(reltablespace, pg_class_desc,
 										relpersistence);
+
+			/* An OID from new_oid_hook may name an existing file */
+			if (NewOidNeedsRelFileNumber() && RELKIND_HAS_STORAGE(relkind))
+				relfilenumber = GetNewRelFileNumber(reltablespace, NULL,
+													relpersistence);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 99413556572..f12059a3240 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -976,6 +976,12 @@ index_create(Relation heapRelation,
 		{
 			indexRelationId =
 				GetNewRelFileNumber(tableSpaceId, pg_class, relpersistence);
+
+			/* An OID from new_oid_hook may name an existing file */
+			if (NewOidNeedsRelFileNumber() && create_storage &&
+				RELKIND_HAS_STORAGE(relkind))
+				relFileNumber = GetNewRelFileNumber(tableSpaceId, NULL,
+													relpersistence);
 		}
 	}
 
diff --git a/src/include/catalog/catalog.h b/src/include/catalog/catalog.h
index feab3982cf5..02dcec0e0c1 100644
--- a/src/include/catalog/catalog.h
+++ b/src/include/catalog/catalog.h
@@ -46,5 +46,17 @@ extern Oid	GetNewOidWithIndex(Relation relation, Oid indexId,
 extern RelFileNumber GetNewRelFileNumber(Oid reltablespace,
 										 Relation pg_class,
 										 char relpersistence);
+extern bool NewOidNeedsRelFileNumber(void);
+
+/*
+ * Hook for an extension to propose the OID of a new catalog row, see
+ * GetNewOidWithIndex().  Returning InvalidOid leaves the choice to the
+ * server.  NEW_OID_HOOK_VERSION tells extensions that the hook exists.
+ */
+#define NEW_OID_HOOK_VERSION 1
+
+typedef Oid (*new_oid_hook_type) (Relation relation, Oid indexId,
+								  AttrNumber oidcolumn);
+extern PGDLLIMPORT new_oid_hook_type new_oid_hook;
 
 #endif							/* CATALOG_H */
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6c1504ac932..d06ecdfb8d5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -39,6 +39,7 @@ SUBDIRS = \
 		  test_lfind \
 		  test_lwlock_tranches \
 		  test_misc \
+		  test_new_oid_hook \
 		  test_oat_hooks \
 		  test_parser \
 		  test_pg_dump \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index ef3cdb8faf9..b2719a16a45 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -41,6 +41,7 @@ subdir('test_json_parser')
 subdir('test_lfind')
 subdir('test_lwlock_tranches')
 subdir('test_misc')
+subdir('test_new_oid_hook')
 subdir('test_oat_hooks')
 subdir('test_parser')
 subdir('test_pg_dump')
diff --git a/src/test/modules/test_new_oid_hook/Makefile b/src/test/modules/test_new_oid_hook/Makefile
new file mode 100644
index 00000000000..945dd7de3cb
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/test_new_oid_hook/Makefile
+
+MODULES = test_new_oid_hook
+PGFILEDESC = "test_new_oid_hook - test code for new_oid_hook"
+
+EXTENSION = test_new_oid_hook
+DATA = test_new_oid_hook--1.0.sql
+
+REGRESS = test_new_oid_hook
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_new_oid_hook
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_new_oid_hook/README b/src/test/modules/test_new_oid_hook/README
new file mode 100644
index 00000000000..252a7811d15
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/README
@@ -0,0 +1,9 @@
+test_new_oid_hook tests new_oid_hook, through which an extension proposes
+the OID of a new catalog row.
+
+Functions
+=========
+test_new_oid_hook_propose(catalog regclass, proposed oid)
+    RETURNS void
+
+Makes the hook propose the OID once, for the next new row of the catalog.
diff --git a/src/test/modules/test_new_oid_hook/expected/test_new_oid_hook.out b/src/test/modules/test_new_oid_hook/expected/test_new_oid_hook.out
new file mode 100644
index 00000000000..7f21f7973e9
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/expected/test_new_oid_hook.out
@@ -0,0 +1,102 @@
+CREATE EXTENSION test_new_oid_hook;
+-- A proposed OID is used, and the relation's file is named after it
+SELECT test_new_oid_hook_propose('pg_class', 4000000000);
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE TABLE proposed_oid_table (a int);
+SELECT oid, relfilenode FROM pg_class WHERE relname = 'proposed_oid_table';
+    oid     | relfilenode 
+------------+-------------
+ 4000000000 |  4000000000
+(1 row)
+
+-- Also for catalogs other than pg_class
+SELECT test_new_oid_hook_propose('pg_namespace', 4000000001);
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE SCHEMA proposed_oid_schema;
+SELECT oid FROM pg_namespace WHERE nspname = 'proposed_oid_schema';
+    oid     
+------------
+ 4000000001
+(1 row)
+
+-- A proposed OID in use is an error, not a duplicate
+SELECT test_new_oid_hook_propose('pg_class', 4000000000);
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE TABLE proposed_oid_in_use (a int);
+ERROR:  proposed OID 4000000000 is already in use in relation "pg_class"
+SELECT count(*) FROM pg_class WHERE oid = 4000000000;
+ count 
+-------
+     1
+(1 row)
+
+-- So is one in the range of system objects
+SELECT test_new_oid_hook_propose('pg_class', 100);
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE TABLE proposed_oid_system (a int);
+ERROR:  proposed OID 100 for relation "pg_class" is in the range of system objects
+-- A proposed OID that names an existing file gets a relfilenumber of its own
+CREATE TABLE truncated_table (a int);
+TRUNCATE truncated_table;
+SELECT test_new_oid_hook_propose('pg_class', relfilenode)
+  FROM pg_class WHERE relname = 'truncated_table';
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE TABLE proposed_oid_file (a int);
+SELECT p.oid = r.relfilenode AS oid_names_file,
+       p.relfilenode <> p.oid AS own_relfilenode
+  FROM pg_class p, pg_class r
+  WHERE p.relname = 'proposed_oid_file' AND r.relname = 'truncated_table';
+ oid_names_file | own_relfilenode 
+----------------+-----------------
+ t              | t
+(1 row)
+
+INSERT INTO proposed_oid_file VALUES (1);
+SELECT * FROM proposed_oid_file;
+ a 
+---
+ 1
+(1 row)
+
+-- The same for an index, with a file the table above did not take
+TRUNCATE truncated_table;
+SELECT test_new_oid_hook_propose('pg_class', relfilenode)
+  FROM pg_class WHERE relname = 'truncated_table';
+ test_new_oid_hook_propose 
+---------------------------
+ 
+(1 row)
+
+CREATE INDEX proposed_oid_index ON proposed_oid_table (a);
+SELECT p.oid = r.relfilenode AS oid_names_file,
+       p.relfilenode <> p.oid AS own_relfilenode
+  FROM pg_class p, pg_class r
+  WHERE p.relname = 'proposed_oid_index' AND r.relname = 'truncated_table';
+ oid_names_file | own_relfilenode 
+----------------+-----------------
+ t              | t
+(1 row)
+
+DROP TABLE proposed_oid_table, proposed_oid_file, truncated_table;
+DROP SCHEMA proposed_oid_schema;
+DROP EXTENSION test_new_oid_hook;
diff --git a/src/test/modules/test_new_oid_hook/meson.build b/src/test/modules/test_new_oid_hook/meson.build
new file mode 100644
index 00000000000..f1e03686c41
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_new_oid_hook_sources = files(
+  'test_new_oid_hook.c',
+)
+
+if host_system == 'windows'
+  test_new_oid_hook_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_new_oid_hook',
+    '--FILEDESC', 'test_new_oid_hook - test code for new_oid_hook',])
+endif
+
+test_new_oid_hook = shared_module('test_new_oid_hook',
+  test_new_oid_hook_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_new_oid_hook
+
+test_install_data += files(
+  'test_new_oid_hook.control',
+  'test_new_oid_hook--1.0.sql',
+)
+
+tests += {
+  'name': 'test_new_oid_hook',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_new_oid_hook',
+    ],
+  },
+}
diff --git a/src/test/modules/test_new_oid_hook/sql/test_new_oid_hook.sql b/src/test/modules/test_new_oid_hook/sql/test_new_oid_hook.sql
new file mode 100644
index 00000000000..c22b89a9c15
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/sql/test_new_oid_hook.sql
@@ -0,0 +1,47 @@
+CREATE EXTENSION test_new_oid_hook;
+
+-- A proposed OID is used, and the relation's file is named after it
+SELECT test_new_oid_hook_propose('pg_class', 4000000000);
+CREATE TABLE proposed_oid_table (a int);
+SELECT oid, relfilenode FROM pg_class WHERE relname = 'proposed_oid_table';
+
+-- Also for catalogs other than pg_class
+SELECT test_new_oid_hook_propose('pg_namespace', 4000000001);
+CREATE SCHEMA proposed_oid_schema;
+SELECT oid FROM pg_namespace WHERE nspname = 'proposed_oid_schema';
+
+-- A proposed OID in use is an error, not a duplicate
+SELECT test_new_oid_hook_propose('pg_class', 4000000000);
+CREATE TABLE proposed_oid_in_use (a int);
+SELECT count(*) FROM pg_class WHERE oid = 4000000000;
+
+-- So is one in the range of system objects
+SELECT test_new_oid_hook_propose('pg_class', 100);
+CREATE TABLE proposed_oid_system (a int);
+
+-- A proposed OID that names an existing file gets a relfilenumber of its own
+CREATE TABLE truncated_table (a int);
+TRUNCATE truncated_table;
+SELECT test_new_oid_hook_propose('pg_class', relfilenode)
+  FROM pg_class WHERE relname = 'truncated_table';
+CREATE TABLE proposed_oid_file (a int);
+SELECT p.oid = r.relfilenode AS oid_names_file,
+       p.relfilenode <> p.oid AS own_relfilenode
+  FROM pg_class p, pg_class r
+  WHERE p.relname = 'proposed_oid_file' AND r.relname = 'truncated_table';
+INSERT INTO proposed_oid_file VALUES (1);
+SELECT * FROM proposed_oid_file;
+
+-- The same for an index, with a file the table above did not take
+TRUNCATE truncated_table;
+SELECT test_new_oid_hook_propose('pg_class', relfilenode)
+  FROM pg_class WHERE relname = 'truncated_table';
+CREATE INDEX proposed_oid_index ON proposed_oid_table (a);
+SELECT p.oid = r.relfilenode AS oid_names_file,
+       p.relfilenode <> p.oid AS own_relfilenode
+  FROM pg_class p, pg_class r
+  WHERE p.relname = 'proposed_oid_index' AND r.relname = 'truncated_table';
+
+DROP TABLE proposed_oid_table, proposed_oid_file, truncated_table;
+DROP SCHEMA proposed_oid_schema;
+DROP EXTENSION test_new_oid_hook;
diff --git a/src/test/modules/test_new_oid_hook/test_new_oid_hook--1.0.sql b/src/test/modules/test_new_oid_hook/test_new_oid_hook--1.0.sql
new file mode 100644
index 00000000000..2ca53cac7c8
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/test_new_oid_hook--1.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_new_oid_hook/test_new_oid_hook--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_new_oid_hook" to load this file. \quit
+
+CREATE FUNCTION test_new_oid_hook_propose(catalog regclass, proposed oid)
+  RETURNS void STRICT
+  AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_new_oid_hook/test_new_oid_hook.c b/src/test/modules/test_new_oid_hook/test_new_oid_hook.c
new file mode 100644
index 00000000000..04575614b8a
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/test_new_oid_hook.c
@@ -0,0 +1,61 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_new_oid_hook.c
+ *		Code for testing new_oid_hook.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_new_oid_hook/test_new_oid_hook.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "catalog/catalog.h"
+#include "fmgr.h"
+#include "utils/rel.h"
+
+PG_MODULE_MAGIC;
+
+/* The OID to propose once, for a new row of this catalog */
+static Oid	proposed_catalog = InvalidOid;
+static Oid	proposed_oid = InvalidOid;
+
+static new_oid_hook_type prev_new_oid_hook = NULL;
+
+static Oid
+test_new_oid(Relation relation, Oid indexId, AttrNumber oidcolumn)
+{
+	if (OidIsValid(proposed_oid) &&
+		RelationGetRelid(relation) == proposed_catalog)
+	{
+		Oid			result = proposed_oid;
+
+		proposed_oid = InvalidOid;
+		return result;
+	}
+
+	if (prev_new_oid_hook)
+		return prev_new_oid_hook(relation, indexId, oidcolumn);
+
+	return InvalidOid;
+}
+
+PG_FUNCTION_INFO_V1(test_new_oid_hook_propose);
+Datum
+test_new_oid_hook_propose(PG_FUNCTION_ARGS)
+{
+	proposed_catalog = PG_GETARG_OID(0);
+	proposed_oid = PG_GETARG_OID(1);
+
+	PG_RETURN_VOID();
+}
+
+void
+_PG_init(void)
+{
+	prev_new_oid_hook = new_oid_hook;
+	new_oid_hook = test_new_oid;
+}
diff --git a/src/test/modules/test_new_oid_hook/test_new_oid_hook.control b/src/test/modules/test_new_oid_hook/test_new_oid_hook.control
new file mode 100644
index 00000000000..c83e22c18b1
--- /dev/null
+++ b/src/test/modules/test_new_oid_hook/test_new_oid_hook.control
@@ -0,0 +1,4 @@
+comment = 'Test code for new_oid_hook'
+default_version = '1.0'
+module_pathname = '$libdir/test_new_oid_hook'
+relocatable = true
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cfef572df80..31b53e15e99 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3970,6 +3970,7 @@ multirange_unnest_fctx
 mxact
 needs_fmgr_hook_type
 network_sortsupport_state
+new_oid_hook_type
 nl_item
 nodeitem
 normal_rand_fctx
-- 
2.34.1

