Hi hackers,

The attached patch adds a hook through which an extension may propose
the OID of a new catalog row.  I'd like to hear whether a hook like this
could be acceptable, before I spend more time on it.

Motivation
----------

I am working on an extension that runs a query across several
PostgreSQL servers: a coordinator plans it and sends parts of the plan
to the other servers.  A plan refers to tables, types, functions and
operators by OID, so the same object must have the same OID on every
server.  Sending SQL instead of plans avoids that, but it means
planning again on every server, and getting the same plan everywhere is
not guaranteed.

DDL runs on all servers, so they create the same objects in the same
order, but nothing makes the OID counters agree: any temporary table or
other object created on one server only moves its counter.  Greenplum
and its forks solve this by patching the server so that the coordinator
dispatches the OIDs it assigned together with the DDL; the segments
take them instead of generating their own.  An extension cannot do that
today.

The closest existing mechanism is binary_upgrade_set_next_*_oid(), but
it works only in binary-upgrade mode and covers only some catalogs.

The patch
---------

GetNewOidWithIndex() calls new_oid_hook, if it is set, before
generating an OID:

    typedef Oid (*new_oid_hook_type) (Relation relation, Oid indexId,
                                      AttrNumber oidcolumn);

InvalidOid leaves the choice to the server, as now.  A valid OID is
checked like any other candidate: it must not be in use (the same index
scan with SnapshotAny as the loop does), and it must not be below
FirstNormalObjectId.  Unlike a generated candidate, a proposed OID that
fails either check is an error rather than a reason to try another one,
since the extension would accept no other.  So the hook cannot create
duplicate OIDs or OIDs in the range of system objects.

The hook is not called in bootstrap mode, during binary upgrade (which
assigns OIDs of its own), or for TOAST value OIDs, which are not
catalog rows.

There is one more case where a relation OID can be in use: a file of
that number may exist already, as a relfilenumber is not always equal
to the OID of its relation (e.g. after TRUNCATE).  GetNewRelFileNumber()
keeps a proposed OID in that case, and NewOidNeedsRelFileNumber() tells
heap_create_with_catalog() and index_create() to take a separate
relfilenumber from a second call.  The relation then just starts with
relfilenode <> oid, which is a state it would reach anyway after the
first rewrite.

NEW_OID_HOOK_VERSION lets an extension check at compile time whether
the hook exists, so it can fall back to something else when built
against a server without it.

The core part is about 60 lines plus comments.  A new test module,
src/test/modules/test_new_oid_hook, proposes OIDs for pg_class and
pg_namespace and checks the error cases and the file collision for a
table and an index.

Questions
---------

- Is a hook here acceptable at all, or would you rather extend the
  binary-upgrade mechanism ("the next OID for this catalog is X") to
  normal operation?
- The hook gets the catalog but not the object being created.  That is
  enough for my case, since the coordinator knows which OIDs to expect
  in which order, but a richer interface (e.g. the new tuple) is
  possible.
- Should a proposed OID in use be an error, as now, or should the
  server fall back to a generated one and let the extension notice?

The patch applies to master as of 4848fd6752a and passes check-world on
Linux, macOS and Windows (MSVC and MinGW) in the GitHub Actions CI that
comes with the tree.

Thanks,
Georgy Shelkovy
From 5b61cea77f6747c2fb94650a41821e68d94f50ef Mon Sep 17 00:00:00 2001
From: Georgy Shelkovy <[email protected]>
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 <[email protected]>
---
 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

Reply via email to