From 4c0c58d28ef7a11947b58900252c11f30165277d Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Wed, 25 Feb 2026 02:16:16 +0100
Subject: [PATCH v1 3/5] Deduplicate some index attributes in the relcache

Many indexes have the same opclasses; so this deduplicates all fields
that are directly dependent on the selected opclasses.  This can save
a significant amount of memory when partitioned indexes are involved,
as those always have the same definition.

I have a few ideas on how to implement this for other fields as well,
but that's not needed in this patch.

This does come at a cost of a slight increase in the amount of work
required to load index relations into catalog caches; but I'd consider
that a worthy trade-off.
---
 src/backend/access/index/indexam.c |  23 +-
 src/backend/utils/cache/relcache.c | 872 ++++++++++++++++++++++-------
 src/backend/utils/fmgr/fmgr.c      |  96 +++-
 src/include/c.h                    |   5 +
 src/include/fmgr.h                 |   7 +-
 src/include/nodes/pathnodes.h      |   2 +-
 src/include/utils/rel.h            |   2 +-
 src/include/utils/relcache.h       |   2 +-
 8 files changed, 804 insertions(+), 205 deletions(-)

diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 0bd0dd9a2f3..e3e6db75557 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -909,6 +909,7 @@ index_getprocinfo(Relation irel,
 	{
 		const RegProcedure *loc = irel->rd_support;
 		RegProcedure procId;
+		Node	   *opcoptions;
 
 		Assert(loc != NULL);
 
@@ -924,17 +925,25 @@ index_getprocinfo(Relation irel,
 			elog(ERROR, "missing support function %d for attribute %d of index \"%s\"",
 				 procnum, attnum, RelationGetRelationName(irel));
 
+		/*
+		 * irel->rd_supportinfo[].fn_expr is pre-populated during cache
+		 * initialization; though only after optsproc has been used.
+		 */
+		opcoptions = locinfo->fn_expr;
 		fmgr_info_cxt(procId, locinfo, irel->rd_indexcxt);
 
 		if (procnum != optsproc)
 		{
-			/* Initialize locinfo->fn_expr with opclass options Const */
-			bytea	  **attoptions = RelationGetIndexAttOptions(irel, false);
-			MemoryContext oldcxt = MemoryContextSwitchTo(irel->rd_indexcxt);
-
-			set_fn_opclass_options(locinfo, attoptions[attnum - 1]);
-
-			MemoryContextSwitchTo(oldcxt);
+			/*
+			 * Even without attoptions, opcoptions here would not be NULL,
+			 * but instead would have ((Const *) opcoptions)->constisnull.
+			 */
+			if (opcoptions == NULL)
+				elog(PANIC, "Invalid opcoptions for valid Relation: NULL instead of Const<null>");
+			if (nodeTag(opcoptions) != T_Const)
+				elog(PANIC, "Unexpected node tag for opcoptions: %d, expected %d",
+					 nodeTag(opcoptions), T_Const);
+			locinfo->fn_expr = opcoptions;
 		}
 	}
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9767d78782e..0d6badb9f01 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -68,6 +68,7 @@
 #include "commands/policy.h"
 #include "commands/publicationcmds.h"
 #include "commands/trigger.h"
+#include "common/hashfn.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -93,7 +94,7 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
-#define RELCACHE_INIT_FILEMAGIC		0x573266	/* version ID value */
+#define RELCACHE_INIT_FILEMAGIC		0x573267	/* version ID value */
 
 /*
  * Whether to bother checking if relation cache memory needs to be freed
@@ -137,6 +138,28 @@ typedef struct relidcacheent
 
 static HTAB *RelationIdCache;
 
+typedef struct RelShapeCacheKey
+{
+	AttrNumber	nkeyatts;
+	uint16		nsupport;
+	/* opclass-specific data */
+	const Oid  *opclass;
+	const bytea *const *opcoptions;		/* Treated as an array of NULL
+										 * elements when NULL */
+} RelShapeCacheKey;
+
+typedef struct RelShapeCacheEnt
+{
+	RelShapeCacheKey key;
+	int			rsc_refcount;			/* refcount */
+	const Oid  *rsc_opfamily;			/* opfamilies of key.opclass */
+	const Oid  *rsc_opcintype;
+	const RegProcedure *rsc_support;			/* support functions */
+	FmgrInfo   *rsc_supportinfo;		/* can be updated */
+} RelShapeCacheEnt;
+
+static HTAB *RelShapeCache;
+
 /*
  * This flag is false until we have prepared the critical relcache entries
  * that are needed to do indexscans on the tables read by relcache building.
@@ -294,6 +317,7 @@ static void AtEOXact_cleanup(Relation relation, bool isCommit);
 static void AtEOSubXact_cleanup(Relation relation, bool isCommit,
 								SubTransactionId mySubid, SubTransactionId parentSubid);
 static bool load_relcache_init_file(bool shared);
+static bool load_relcache_init_index(Relation relation, FILE *fp);
 static void write_relcache_init_file(bool shared);
 static void write_item(const void *data, Size len, FILE *fp);
 
@@ -315,13 +339,21 @@ static void CheckNNConstraintFetch(Relation relation);
 static int	CheckConstraintCmp(const void *a, const void *b);
 static void InitIndexAmRoutine(Relation relation);
 static void IndexSupportInitialize(oidvector *indclass, AttrNumber nKeyAtts, StrategyNumber maxSupportNumber,
-								   MemoryContext indexcxt, const Oid **rd_opfamily, const Oid **rd_opcintype,
-								   const RegProcedure **rd_support, FmgrInfo **rd_supportinfo);
+								   const Oid **rd_opfamily, const Oid **rd_opcintype,
+								   const RegProcedure **rd_support, FmgrInfo **rd_supportinfo,
+								   const bytea *const **rd_opcoptions);
+static void IndexSupportDeregister(oidvector *indclass, AttrNumber nKeyAtts,
+								   StrategyNumber maxSupportNumber,
+								   const bytea * const*rd_opcoptions);
+static bytea **IndexRelationGetAttOptions(Relation relation);
 static OpClassCacheEnt *LookupOpclassInfo(Oid operatorClassOid,
 										  StrategyNumber numSupport);
 static void RelationCacheInitFileRemoveInDir(const char *tblspcpath);
 static void unlink_initfile(const char *initfilename, int elevel);
 
+static uint32 HashRelShapeKey(const void *key, Size keysize);
+static int CmpRelShapeKey(const void *key1, const void *key2, Size keysize);
+static void *CopyRelShapeKey(void *dest, const void *src, Size keysize);
 
 /*
  *		ScanPgRelation
@@ -1448,6 +1480,8 @@ RelationInitIndexAccessInfo(Relation relation)
 	int			indnatts;
 	int			indnkeyatts;
 	uint16		amsupport;
+	const bytea *const *opcoptions;
+	bool		hasopcoptions;
 
 	/*
 	 * Make a copy of the pg_index entry for the index.  Since pg_index
@@ -1542,14 +1576,56 @@ RelationInitIndexAccessInfo(Relation relation)
 
 	/*
 	 * Fill the support procedure OID array, as well as the info about
-	 * opfamilies and opclass input types.  (aminfo and supportinfo are left
-	 * as zeroes, and are filled on-the-fly when used)
+	 * opfamilies and opclass input types.  (opcoptions and supportinfo
+	 * are left as zeroes; opcoptions is filled down below, supportinfo
+	 * mostly only on access)
 	 */
-	IndexSupportInitialize(indclass, indnkeyatts, amsupport, indexcxt,
+	IndexSupportInitialize(indclass, indnkeyatts, amsupport,
 						   &relation->rd_opfamily,
 						   &relation->rd_opcintype,
 						   &relation->rd_support,
-						   &relation->rd_supportinfo);
+						   &relation->rd_supportinfo,
+						   &relation->rd_opcoptions);
+
+	opcoptions = (const bytea *const *) IndexRelationGetAttOptions(relation);
+
+	for (int i = 0; i < indnkeyatts; i++)
+	{
+		if (opcoptions[i] == NULL)
+			continue;
+		hasopcoptions = true;
+	}
+
+	if (hasopcoptions)
+	{
+		IndexSupportDeregister(indclass, indnkeyatts, amsupport,
+							   relation->rd_opcoptions);
+
+		relation->rd_opcoptions = opcoptions;
+
+		/*
+		 * This fills in all fields, again; now whilst taking the new opcoptions
+		 * into account.
+		 */
+		IndexSupportInitialize(indclass, indnkeyatts, amsupport,
+							   &relation->rd_opfamily,
+							   &relation->rd_opcintype,
+							   &relation->rd_support,
+							   &relation->rd_supportinfo,
+							   &relation->rd_opcoptions);
+	}
+
+	/*
+	 * Opcoptions was either copied into the cache context, or wasn't used;
+	 * in either case it's now unused and we should free it.
+	 */
+	Assert(opcoptions != relation->rd_opcoptions);
+	for (int i = 0; i < indnkeyatts; i++)
+	{
+		if (opcoptions[i] != NULL)
+			pfree(unconstify(bytea *, opcoptions[i]));
+	}
+	pfree(unconstify_array(const bytea *, opcoptions));
 
 	/*
 	 * Similarly extract indoption and copy it to the cache entry
@@ -1562,8 +1638,6 @@ RelationInitIndexAccessInfo(Relation relation)
 	indoption = (int2vector *) DatumGetPointer(indoptionDatum);
 	memcpy(relation->rd_indoption, indoption->values, indnkeyatts * sizeof(int16));
 
-	(void) RelationGetIndexAttOptions(relation, false);
-
 	/*
 	 * expressions, predicate, exclusion caches will be filled later
 	 */
@@ -1591,58 +1665,170 @@ RelationInitIndexAccessInfo(Relation relation)
 static void
 IndexSupportInitialize(oidvector *indclass, AttrNumber nKeyAtts,
 					   StrategyNumber maxSupportNumber,
-					   MemoryContext indexcxt,
 					   const Oid **rd_opfamily,
 					   const Oid **rd_opcintype,
 					   const RegProcedure **rd_support,
-					   FmgrInfo **rd_supportinfo)
+					   FmgrInfo **rd_supportinfo,
+					   const bytea *const **rd_opcoptions)
 {
-	Oid		   *opFamily;
-	Oid		   *opcInType;
-	RegProcedure *indexSupport;
+	RelShapeCacheKey key = {0};
+	RelShapeCacheEnt *entry;
+	bool			found;
 
-	if (maxSupportNumber > 0)
-	{
-		int		nprocs = maxSupportNumber * nKeyAtts;
-		rd_supportinfo[0] = (FmgrInfo *)
-			MemoryContextAllocZero(indexcxt, nprocs * sizeof(FmgrInfo));
-		indexSupport = (RegProcedure *)
-			MemoryContextAllocZero(indexcxt, nprocs * sizeof(RegProcedure));
-	}
-	else
-	{
-		*rd_supportinfo = NULL;
-		indexSupport = NULL;
-	}
+	key.nkeyatts = nKeyAtts;
+	key.nsupport = maxSupportNumber;
+	key.opclass = indclass->values;
+	key.opcoptions = *rd_opcoptions;
+
+	entry = hash_search(RelShapeCache, &key, HASH_ENTER, &found);
 
-	opFamily = (Oid *)
-		MemoryContextAllocZero(indexcxt, nKeyAtts * sizeof(Oid));
-	opcInType = (Oid *)
-		MemoryContextAllocZero(indexcxt, nKeyAtts * sizeof(Oid));
+	Assert(entry != NULL);
 
-	for (int attIndex = 0; attIndex < nKeyAtts; attIndex++)
+	if (!found)
 	{
-		OpClassCacheEnt *opcentry;
+		Oid		   *opFamily;
+		Oid		   *opcInType;
+		RegProcedure *support;
+		FmgrInfo   *supportinfo;
+
+		if (maxSupportNumber > 0)
+		{
+			supportinfo = (FmgrInfo *)
+				MemoryContextAllocZero(CacheMemoryContext,
+									   maxSupportNumber * nKeyAtts * sizeof(FmgrInfo));
+			support = (RegProcedure *)
+				MemoryContextAllocZero(CacheMemoryContext,
+									   maxSupportNumber * nKeyAtts * sizeof(RegProcedure));
+		}
+		else
+		{
+			supportinfo = NULL;
+			support = NULL;
+		}
+
+		opFamily = (Oid *)
+			MemoryContextAllocZero(CacheMemoryContext,
+								   nKeyAtts * sizeof(Oid));
+		opcInType = (Oid *)
+			MemoryContextAllocZero(CacheMemoryContext,
+								   nKeyAtts * sizeof(Oid));
+
+		for (int attIndex = 0; attIndex < nKeyAtts; attIndex++)
+		{
+			OpClassCacheEnt *opcentry;
 
-		if (!OidIsValid(indclass->values[attIndex]))
-			elog(ERROR, "bogus pg_index tuple");
+			if (!OidIsValid(indclass->values[attIndex]))
+				elog(ERROR, "bogus pg_index tuple");
 
-		/* look up the info for this opclass, using a cache */
-		opcentry = LookupOpclassInfo(indclass->values[attIndex],
-									 maxSupportNumber);
+			/* look up the info for this opclass, using a cache */
+			opcentry = LookupOpclassInfo(indclass->values[attIndex],
+										 maxSupportNumber);
+
+			/* copy cached data into relcache entry */
+			opFamily[attIndex] = opcentry->opcfamily;
+			opcInType[attIndex] = opcentry->opcintype;
+
+			if (maxSupportNumber > 0)
+			{
+				/* copy support proc IDs into the support proc array */
+				memcpy(&support[attIndex * maxSupportNumber],
+					   opcentry->supportProcs,
+					   maxSupportNumber * sizeof(RegProcedure));
+			}
+		}
 
-		/* copy cached data into relcache entry */
-		opFamily[attIndex] = opcentry->opcfamily;
-		opcInType[attIndex] = opcentry->opcintype;
 		if (maxSupportNumber > 0)
-			memcpy(&indexSupport[attIndex * maxSupportNumber],
-				   opcentry->supportProcs,
-				   maxSupportNumber * sizeof(RegProcedure));
+		{
+			MemoryContext cxt = MemoryContextSwitchTo(CacheMemoryContext);
+			set_fn_opclass_options_bulk(supportinfo,
+										entry->key.opcoptions,
+										maxSupportNumber,
+										nKeyAtts);
+			MemoryContextSwitchTo(cxt);
+		}
+
+		entry->rsc_opfamily = opFamily;
+		entry->rsc_opcintype = opcInType;
+		entry->rsc_support = support;
+		entry->rsc_supportinfo = supportinfo;
+		entry->rsc_refcount = 1;
+
+		Assert(maxSupportNumber == 0 || entry->rsc_supportinfo);
+		Assert(maxSupportNumber == 0 || entry->rsc_support);
+		Assert(maxSupportNumber == 0 || entry->key.opcoptions);
 	}
 
-	rd_support[0] = indexSupport;
-	rd_opcintype[0] = opcInType;
-	rd_opfamily[0] = opFamily;
+	entry->rsc_refcount++;
+
+	*rd_opcoptions = entry->key.opcoptions;
+
+	*rd_opfamily = entry->rsc_opfamily;
+	*rd_opcintype = entry->rsc_opcintype;
+	*rd_support = entry->rsc_support;
+	*rd_supportinfo = entry->rsc_supportinfo;
+}
+
+static void
+IndexSupportDeregister(oidvector *indclass, AttrNumber nKeyAtts,
+					   StrategyNumber maxSupportNumber,
+					   const bytea *const *rd_opcoptions)
+{
+	RelShapeCacheKey key = {0};
+	RelShapeCacheEnt *entry;
+	bool			found;
+
+	key.nkeyatts = nKeyAtts;
+	key.nsupport = maxSupportNumber;
+	key.opclass = indclass->values;
+	key.opcoptions = rd_opcoptions;
+
+	entry = hash_search(RelShapeCache, &key, HASH_FIND, &found);
+
+	Assert(found);
+
+	entry->rsc_refcount--;
+
+	if (entry->rsc_refcount > 1)
+		return;
+
+	Assert(entry->rsc_refcount == 1);
+	/*
+	 * Apart from key.opclass[], which is taken from an oidvector, all key
+	 * fields are stored in RelationData and are expected to be passed
+	 * directly by the caller.
+	 */
+	Assert(entry->key.opcoptions == key.opcoptions);
+	Assert(entry->key.opclass != key.opclass);
+	
+	/*
+	 * Store the opclass reference, so we can use it after removing the hash
+	 * entry.
+	 */
+	key.opclass = entry->key.opclass;
+
+	/* free the cached elements */
+	pfree(unconstify(Oid *, entry->rsc_opfamily));
+	pfree(unconstify(Oid *, entry->rsc_opcintype));
+	pfree(unconstify(RegProcedure *, entry->rsc_support));
+
+	clear_fn_opclass_options_bulk(entry->rsc_supportinfo,
+								  entry->key.nkeyatts * entry->key.nsupport,
+								  entry->key.nkeyatts);
+
+	pfree(entry->rsc_supportinfo);
+
+	/* Remove the hash entry */
+	entry = hash_search(RelShapeCache, &key, HASH_REMOVE, &found);
+	Assert(found);
+
+	/* free the key's allocated elements */
+	pfree(unconstify(Oid *, key.opclass));
+	for (int i = 0; i < key.nkeyatts; i++)
+	{
+		if (key.opcoptions[i] != NULL)
+			pfree(unconstify(bytea *, key.opcoptions[i]));
+	}
+	pfree(unconstify_array(const bytea *, key.opcoptions));
 }
 
 /*
@@ -2461,6 +2647,28 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
 	/* break mutual link with stats entry */
 	pgstat_unlink_relation(relation);
 
+	if (relation->rd_isvalid && relation->rd_indam)
+	{
+		Datum		indclassDatum;
+		bool		isnull;
+		oidvector  *indclass;
+
+		Assert(relation->rd_indextuple != NULL);
+
+		indclassDatum = fastgetattr(relation->rd_indextuple,
+								   Anum_pg_index_indclass,
+								   GetPgIndexDescriptor(),
+								   &isnull);
+		Assert(!isnull);
+
+		indclass = (oidvector *) DatumGetPointer(indclassDatum);
+
+		IndexSupportDeregister(indclass,
+							   IndexRelationGetNumberOfKeyAttributes(relation),
+							   relation->rd_indam->amsupport,
+							   relation->rd_opcoptions);
+	}
+
 	/*
 	 * Free all the subsidiary data structures of the relcache entry, then the
 	 * entry itself.
@@ -4030,6 +4238,21 @@ RelationCacheInitialize(void)
 	RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,
 								  &ctl, HASH_ELEM | HASH_BLOBS);
 
+	/*
+	 * create hashtable that indexes and caches relation shapes
+	 */
+	ctl.keysize = sizeof(RelShapeCacheKey);
+	ctl.entrysize = sizeof(RelShapeCacheEnt);
+	ctl.keycopy = CopyRelShapeKey;
+	ctl.hash = HashRelShapeKey;
+	ctl.match = CmpRelShapeKey;
+	ctl.hcxt = CacheMemoryContext;
+
+	RelShapeCache = hash_create("Relation shape cache", INITRELCACHESIZE,
+								&ctl, HASH_ELEM |
+								HASH_FUNCTION | HASH_COMPARE | HASH_KEYCOPY |
+								HASH_CONTEXT);
+
 	/*
 	 * reserve enough in_progress_list slots for many cases
 	 */
@@ -4401,6 +4624,192 @@ RelationCacheInitializePhase3(void)
 	}
 }
 
+static uint32
+HashRelShapeKey(const void *key, Size keysize)
+{
+	const RelShapeCacheKey *rskey = key;
+	uint32		hdr_hash, opc_hash, opcopt_hash;
+
+	hdr_hash = hash_combine(hash_uint32(rskey->nkeyatts),
+							hash_uint32(rskey->nsupport));
+	opc_hash = hash_any((void *) rskey->opclass,
+						sizeof(Oid) * rskey->nkeyatts);
+
+	/*
+	 * Opcoptions hashing is special; as it may be NULL in search keys.
+	 * Additionally, the array (if present) can contain nulls.  We must still
+	 * hash these NULLs, because positionality matters: An index with
+	 * (opts, NULL) must hash differently from one with (NULL, opts) or even
+	 * (opts). If we ignored NULLs in the array, they'd be guaranteed to have
+	 * an equal hash value, causing hash conflicts with performance issues
+	 * as a result.
+	 */
+	opcopt_hash = 0;
+	if (rskey->opcoptions == NULL)
+	{
+		/*
+		 * Only present for search operations, but regardless, we'll have to
+		 * combine the hashes of every (missing) element of the NULL array.
+		 */
+		for (int i = 0; i < rskey->nkeyatts; i++)
+			opcopt_hash = hash_combine(opcopt_hash, 0);
+	}
+	else
+	{
+		for (int i = 0; i < rskey->nkeyatts; i++)
+		{
+			const bytea *opt = rskey->opcoptions[i];
+			uint32	hash;
+
+			if (opt == NULL)
+				hash = 0;
+			else
+			{
+				hash = hash_any((const unsigned char *) VARDATA(opt),
+								(int) VARSIZE(opt));
+			}
+
+			opcopt_hash = hash_combine(opcopt_hash, hash);
+		}
+	}
+
+	return hash_combine(opcopt_hash, hash_combine(hdr_hash, opc_hash));
+}
+
+static int
+CmpRelShapeKey(const void *key1, const void *key2, Size keysize)
+{
+	const RelShapeCacheKey *rskey1 = key1;
+	const RelShapeCacheKey *rskey2 = key2;
+
+	if (rskey1->nkeyatts != rskey2->nkeyatts ||
+		rskey1->nsupport != rskey2->nsupport)
+		return 1;
+
+	if (rskey1->opclass != rskey2->opclass &&
+		memcmp(rskey1->opclass, rskey2->opclass,
+			   sizeof(Oid) * rskey1->nkeyatts) != 0)
+		return 1;
+
+	/* both NULL, or point to the same memory */
+	if (rskey1->opcoptions == rskey2->opcoptions)
+		return 0;
+
+	/*
+	 * One of the two arrays is null; they only match when the allocated
+	 * array's elements are all NULL too
+	 */
+	if (rskey1->opcoptions == NULL || rskey2->opcoptions == NULL)
+	{
+		const bytea *const *opts;
+
+		if (rskey1->opcoptions != NULL)
+		{
+			Assert(rskey2->opcoptions == NULL);
+			opts = rskey1->opcoptions;
+		}
+		else
+		{
+			Assert(rskey2->opcoptions != NULL);
+			opts = rskey2->opcoptions;
+		}
+
+		for (int i = 0; i < rskey1->nkeyatts; i++)
+		{
+			if (opts[i] != NULL)
+				return 1;
+		}
+
+		return 0;
+	}
+
+	/*
+	 * Compare the options elements; now that we've established that
+	 * both arrays are non-null.
+	 */
+	for (int i = 0; i < rskey1->nkeyatts; i++)
+	{
+		const bytea *a = rskey1->opcoptions[i];
+		const bytea *b = rskey2->opcoptions[i];
+
+		/* both elements are NULL, or point to the same value */
+		if (a == b)
+			continue;
+
+		/* not the same: only one of the elements is NULL */
+		if (a == NULL || b == NULL)
+			return 1;
+
+		/* different; if the sizes are different */
+		if (VARSIZE(a) != VARSIZE(b))
+			return 1;
+
+		/* different; if the data in the options is different */
+		if (memcmp(VARDATA(a), VARDATA(b), VARSIZE(a)) != 0)
+			return 1;
+	}
+
+	/* no differences detected */
+	return 0;
+}
+
+static void *
+CopyRelShapeKey(void *dest, const void *src, Size keysize)
+{
+	RelShapeCacheKey *destrskey = dest;
+	const RelShapeCacheKey *srcrskey = src;
+	Oid			   *opclass;
+	const bytea	  **opcoptions;
+
+	destrskey->nkeyatts = srcrskey->nkeyatts;
+	destrskey->nsupport = srcrskey->nsupport;
+
+	opclass = MemoryContextAlloc(CacheMemoryContext,
+								 sizeof(Oid) * srcrskey->nkeyatts);
+	opcoptions = MemoryContextAlloc(CacheMemoryContext,
+									sizeof(bytea *) * srcrskey->nkeyatts);
+
+	memcpy(opclass, srcrskey->opclass, sizeof(Oid) * srcrskey->nkeyatts);
+
+	if (srcrskey->opcoptions != NULL)
+	{
+		for (int i = 0; i < srcrskey->nkeyatts; i++)
+		{
+			const bytea *srcopt = srcrskey->opcoptions[i];
+			Size	srcsize;
+
+			if (srcopt != NULL)
+			{
+				bytea *newopt;
+
+				srcsize = VARSIZE(srcopt);
+				newopt = MemoryContextAlloc(CacheMemoryContext, srcsize + VARHDRSZ);
+				SET_VARSIZE(newopt, srcsize);
+				memcpy(VARDATA(newopt), VARDATA(srcopt), srcsize);
+				opcoptions[i] = newopt;
+			}
+			else
+			{
+				opcoptions[i] = NULL;
+			}
+		}
+	}
+	else
+	{
+		for (int i = 0; i < srcrskey->nkeyatts; i++)
+			opcoptions[i] = NULL;
+	}
+
+	destrskey->opclass = opclass;
+	destrskey->opcoptions = opcoptions;
+
+	Assert(HashRelShapeKey(src, keysize) == HashRelShapeKey(dest, keysize));
+	Assert(CmpRelShapeKey(src, dest, keysize) == 0);
+
+	return destrskey;
+}
+
+
 /*
  * Load one critical system index into the relcache
  *
@@ -6006,13 +6415,13 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 }
 
 static bytea **
-CopyIndexAttOptions(bytea **srcopts, int natts)
+CopyIndexAttOptions(const bytea *const *srcopts, int natts)
 {
 	bytea	  **opts = palloc_array(bytea *, natts);
 
 	for (int i = 0; i < natts; i++)
 	{
-		bytea	   *opt = srcopts[i];
+		const bytea *opt = srcopts[i];
 
 		opts[i] = !opt ? NULL : (bytea *)
 			DatumGetPointer(datumCopy(PointerGetDatum(opt), false, -1));
@@ -6021,29 +6430,19 @@ CopyIndexAttOptions(bytea **srcopts, int natts)
 	return opts;
 }
 
-/*
- * RelationGetIndexAttOptions
- *		get AM/opclass-specific options for an index parsed into a binary form
- */
-bytea	  **
-RelationGetIndexAttOptions(Relation relation, bool copy)
+static bytea	  **
+IndexRelationGetAttOptions(Relation relation)
 {
-	MemoryContext oldcxt;
-	bytea	  **opts = relation->rd_opcoptions;
-	Oid			relid = RelationGetRelid(relation);
-	int			natts = IndexRelationGetNumberOfKeyAttributes(relation);
-	int			i;
-
-	/* Try to copy cached options. */
-	if (opts)
-		return copy ? CopyIndexAttOptions(opts, natts) : opts;
+	bytea **opts;
+	int natts = IndexRelationGetNumberOfKeyAttributes(relation);
+	Oid relid = relation->rd_id;
 
 	/* Get and parse opclass options. */
 	opts = palloc0_array(bytea *, natts);
 
-	for (i = 0; i < natts; i++)
+	if (criticalRelcachesBuilt && relid != AttributeRelidNumIndexId)
 	{
-		if (criticalRelcachesBuilt && relid != AttributeRelidNumIndexId)
+		for (int i = 0; i < natts; i++)
 		{
 			Datum		attoptions = get_attoptions(relid, i + 1);
 
@@ -6054,9 +6453,32 @@ RelationGetIndexAttOptions(Relation relation, bool copy)
 		}
 	}
 
+	return opts;
+}
+
+/*
+ * RelationGetIndexAttOptions
+ *		get AM/opclass-specific options for an index parsed into a binary form
+ */
+const bytea	*const *
+RelationGetIndexAttOptions(Relation relation, bool copy)
+{
+	MemoryContext oldcxt;
+	const bytea	  *const *opts = relation->rd_opcoptions;
+	int			natts = IndexRelationGetNumberOfKeyAttributes(relation);
+	int			i;
+
+	/* Try to copy cached options. */
+	if (opts)
+		return copy ? (const bytea *const *) CopyIndexAttOptions(opts, natts) : opts;
+
+	/* Get and parse opclass options. */
+	opts = (const bytea *const *) IndexRelationGetAttOptions(relation);
+
 	/* Copy parsed options to the cache. */
 	oldcxt = MemoryContextSwitchTo(relation->rd_indexcxt);
-	relation->rd_opcoptions = CopyIndexAttOptions(opts, natts);
+	relation->rd_opcoptions = (const bytea *const *)
+		CopyIndexAttOptions(opts, natts);
 	MemoryContextSwitchTo(oldcxt);
 
 	if (copy)
@@ -6065,10 +6487,10 @@ RelationGetIndexAttOptions(Relation relation, bool copy)
 	for (i = 0; i < natts; i++)
 	{
 		if (opts[i])
-			pfree(opts[i]);
+			pfree(unconstify(bytea *, opts[i]));
 	}
 
-	pfree(opts);
+	pfree(unconstify_array(const bytea *, opts));
 
 	return relation->rd_opcoptions;
 }
@@ -6346,119 +6768,12 @@ load_relcache_init_file(bool shared)
 		 */
 		if (rel->rd_rel->relkind == RELKIND_INDEX)
 		{
-			MemoryContext indexcxt;
-			Oid		   *opfamily;
-			Oid		   *opcintype;
-			RegProcedure *support;
-			int			nsupport;
-			int16	   *indoption;
-			Oid		   *indcollation;
-
 			/* Count nailed indexes to ensure we have 'em all */
 			if (rel->rd_isnailed)
 				nailed_indexes++;
 
-			/* read the pg_index tuple */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-
-			rel->rd_indextuple = (HeapTuple) palloc(len);
-			if (fread(rel->rd_indextuple, 1, len, fp) != len)
-				goto read_failed;
-
-			/* Fix up internal pointers in the tuple -- see heap_copytuple */
-			rel->rd_indextuple->t_data = (HeapTupleHeader) ((char *) rel->rd_indextuple + HEAPTUPLESIZE);
-			rel->rd_index = (Form_pg_index) GETSTRUCT(rel->rd_indextuple);
-
-			/*
-			 * prepare index info context --- parameters should match
-			 * RelationInitIndexAccessInfo
-			 */
-			indexcxt = AllocSetContextCreate(CacheMemoryContext,
-											 "index info",
-											 ALLOCSET_SMALL_SIZES);
-			rel->rd_indexcxt = indexcxt;
-			MemoryContextCopyAndSetIdentifier(indexcxt,
-											  RelationGetRelationName(rel));
-
-			/*
-			 * Now we can fetch the index AM's API struct.  (We can't store
-			 * that in the init file, since it contains function pointers that
-			 * might vary across server executions.  Fortunately, it should be
-			 * safe to call the amhandler even while bootstrapping indexes.)
-			 */
-			InitIndexAmRoutine(rel);
-
-			/* read the vector of opfamily OIDs */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-
-			opfamily = (Oid *) MemoryContextAlloc(indexcxt, len);
-			if (fread(opfamily, 1, len, fp) != len)
-				goto read_failed;
-
-			rel->rd_opfamily = opfamily;
-
-			/* read the vector of opcintype OIDs */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-
-			opcintype = (Oid *) MemoryContextAlloc(indexcxt, len);
-			if (fread(opcintype, 1, len, fp) != len)
-				goto read_failed;
-
-			rel->rd_opcintype = opcintype;
-
-			/* read the vector of support procedure OIDs */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-			support = (RegProcedure *) MemoryContextAlloc(indexcxt, len);
-			if (fread(support, 1, len, fp) != len)
+			if (!load_relcache_init_index(rel, fp))
 				goto read_failed;
-
-			rel->rd_support = support;
-
-			/* read the vector of collation OIDs */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-
-			indcollation = (Oid *) MemoryContextAlloc(indexcxt, len);
-			if (fread(indcollation, 1, len, fp) != len)
-				goto read_failed;
-
-			rel->rd_indcollation = indcollation;
-
-			/* read the vector of indoption values */
-			if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-				goto read_failed;
-
-			indoption = (int16 *) MemoryContextAlloc(indexcxt, len);
-			if (fread(indoption, 1, len, fp) != len)
-				goto read_failed;
-
-			rel->rd_indoption = indoption;
-
-			/* read the vector of opcoptions values */
-			rel->rd_opcoptions = (bytea **)
-				MemoryContextAllocZero(indexcxt, sizeof(*rel->rd_opcoptions) * relform->relnatts);
-
-			for (i = 0; i < relform->relnatts; i++)
-			{
-				if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
-					goto read_failed;
-
-				if (len > 0)
-				{
-					rel->rd_opcoptions[i] = (bytea *) MemoryContextAlloc(indexcxt, len);
-					if (fread(rel->rd_opcoptions[i], 1, len, fp) != len)
-						goto read_failed;
-				}
-			}
-
-			/* set up zeroed fmgr-info vector */
-			nsupport = relform->relnatts * rel->rd_indam->amsupport;
-			rel->rd_supportinfo = (FmgrInfo *)
-				MemoryContextAllocZero(indexcxt, nsupport * sizeof(FmgrInfo));
 		}
 		else
 		{
@@ -6619,6 +6934,171 @@ read_failed:
 	return false;
 }
 
+static bool
+load_relcache_init_index(Relation rel, FILE *fp)
+{
+	MemoryContext indexcxt;
+	RelShapeCacheKey key;
+	const bytea **opcoptions;
+	RelShapeCacheEnt *entry;
+	Oid		   *opfamily;
+	Oid		   *opcintype;
+	RegProcedure *support;
+	int16	   *indoption;
+	Oid		   *indcollation;
+	Oid			indclass[INDEX_MAX_KEYS];
+	Size		len;
+	Datum		indclassDatum;
+	bool		isnull, found;
+
+	/* read the pg_index tuple */
+	if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
+		return false;
+
+	Assert(len >= HEAPTUPLESIZE + SizeofHeapTupleHeader + sizeof(FormData_pg_index));
+
+	rel->rd_indextuple = (HeapTuple) palloc(len);
+	Assert(len != 0);
+
+	if (fread(rel->rd_indextuple, 1, len, fp) != len)
+		return false;
+	Assert(rel->rd_indextuple->t_len >= 0);
+
+	/* Fix up internal pointers in the tuple -- see heap_copytuple */
+	rel->rd_indextuple->t_data = (HeapTupleHeader) (((char *) rel->rd_indextuple) + HEAPTUPLESIZE);
+	rel->rd_index = (Form_pg_index) GETSTRUCT(rel->rd_indextuple);
+
+	/*
+	 * prepare index info context --- parameters should match
+	 * RelationInitIndexAccessInfo
+	 */
+	indexcxt = AllocSetContextCreate(CacheMemoryContext,
+									 "index info",
+									 ALLOCSET_SMALL_SIZES);
+	rel->rd_indexcxt = indexcxt;
+	MemoryContextCopyAndSetIdentifier(indexcxt,
+									  RelationGetRelationName(rel));
+
+	/*
+	 * Now we can fetch the index AM's API struct.  (We can't store
+	 * that in the init file, since it contains function pointers that
+	 * might vary across server executions.  Fortunately, it should be
+	 * safe to call the amhandler even while bootstrapping indexes.)
+	 */
+	InitIndexAmRoutine(rel);
+
+	key.nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+	key.nsupport = rel->rd_indam->amsupport;
+
+	indclassDatum = fastgetattr(rel->rd_indextuple,
+								Anum_pg_index_indclass,
+								GetPgIndexDescriptor(),
+								&isnull);
+	Assert(!isnull);
+	memcpy(&indclass[0],
+		   ((oidvector *) DatumGetPointer(indclassDatum))->values,
+		   sizeof(Oid) * key.nkeyatts);
+	key.opclass = indclass;
+
+	/* read the vector of opcoptions values */
+	opcoptions = (const bytea **)
+		MemoryContextAlloc(CacheMemoryContext, sizeof(const bytea *) * key.nkeyatts);
+
+	/* read the values */
+	for (int i = 0; i < key.nkeyatts; i++)
+	{
+		if (fread(&len, 1, sizeof(len), fp) != sizeof(len))
+			return false;
+
+		if (len > 0)
+		{
+			bytea *read = (bytea *)
+				MemoryContextAlloc(CacheMemoryContext, len);
+
+			Assert(len >= 4);
+
+			if (fread(read, 1, len, fp) != len)
+				return false;
+
+			opcoptions[i] = read;
+		}
+		else
+			opcoptions[i] = NULL;
+	}
+
+	Assert(opcoptions);
+	Assert(key.nkeyatts > 0 && key.nkeyatts <= INDEX_MAX_KEYS);
+	Assert(key.nsupport);
+
+	key.opcoptions = opcoptions;
+	entry = hash_search(RelShapeCache, &key, HASH_ENTER, &found);
+
+#define READ_ITEM(type, name, neachkey, context) \
+	do { \
+		if (fread(&len, 1, sizeof(len), fp) != sizeof(len)) \
+			return false; \
+		\
+		if (len != sizeof(type) * (neachkey) * key.nkeyatts) \
+			return false; \
+		\
+		name = (type *) \
+			MemoryContextAlloc(context, len); \
+		\
+		if (fread(name, 1, len, fp) != len) \
+			return false; \
+	} while (0)
+
+	READ_ITEM(Oid, opfamily, 1, CacheMemoryContext);
+	READ_ITEM(Oid, opcintype, 1, CacheMemoryContext);
+	READ_ITEM(RegProcedure, support, key.nsupport, CacheMemoryContext);
+	READ_ITEM(Oid, indcollation, 1, indexcxt);
+	READ_ITEM(int16, indoption, 1, indexcxt);
+
+	if (!found)
+	{
+		MemoryContext cxt;
+		entry->rsc_refcount = 1;
+		entry->rsc_opfamily = opfamily;
+		entry->rsc_opcintype = opcintype;
+		entry->rsc_support = support;
+		entry->rsc_supportinfo = (FmgrInfo *)
+			MemoryContextAllocZero(CacheMemoryContext,
+								   sizeof(FmgrInfo) * key.nkeyatts * key.nsupport);
+
+		Assert(entry->key.opcoptions);
+		Assert(entry->key.opclass);
+		Assert(entry->rsc_supportinfo || entry->key.nsupport == 0);
+		Assert(entry->rsc_support || entry->key.nsupport == 0);
+
+		cxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+		set_fn_opclass_options_bulk(entry->rsc_supportinfo,
+									entry->key.opcoptions,
+									key.nsupport,
+									key.nkeyatts);
+
+		MemoryContextSwitchTo(cxt);
+	}
+	else
+	{
+		pfree(opfamily);
+		pfree(opcintype);
+		pfree(support);
+	}
+
+	entry->rsc_refcount++;
+
+	rel->rd_opfamily = entry->rsc_opfamily;
+	rel->rd_opcintype = entry->rsc_opcintype;
+	rel->rd_support = entry->rsc_support;
+	rel->rd_indcollation = indcollation;
+	rel->rd_indoption = indoption;
+	rel->rd_opcoptions = entry->key.opcoptions;
+	rel->rd_supportinfo = entry->rsc_supportinfo;
+
+	return true;
+}
+
 /*
  * Write out a new initialization file with the current contents
  * of the relcache (either shared rels or local rels, as indicated).
@@ -6743,46 +7223,54 @@ write_relcache_init_file(bool shared)
 		 */
 		if (rel->rd_rel->relkind == RELKIND_INDEX)
 		{
+			int		nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
+			bool	isnull;
+			Datum	indclassDatum =
+				fastgetattr(rel->rd_indextuple,
+							Anum_pg_index_indclass,
+							GetPgIndexDescriptor(),
+							&isnull);
+			Assert(!isnull);
+			Assert(((oidvector *) DatumGetPointer(indclassDatum))->values[0] != InvalidOid);
+
 			/* write the pg_index tuple */
 			/* we assume this was created by heap_copytuple! */
 			write_item(rel->rd_indextuple,
 					   HEAPTUPLESIZE + rel->rd_indextuple->t_len,
 					   fp);
 
+			/* write the vector of opcoptions values */
+			for (i = 0; i < nkeyatts; i++)
+			{
+				const bytea *opt = rel->rd_opcoptions[i];
+
+				write_item(opt, opt ? VARSIZE(opt) : 0, fp);
+			}
+
 			/* write the vector of opfamily OIDs */
 			write_item(rel->rd_opfamily,
-					   relform->relnatts * sizeof(Oid),
+					   nkeyatts * sizeof(Oid),
 					   fp);
 
 			/* write the vector of opcintype OIDs */
 			write_item(rel->rd_opcintype,
-					   relform->relnatts * sizeof(Oid),
+					   nkeyatts * sizeof(Oid),
 					   fp);
 
 			/* write the vector of support procedure OIDs */
 			write_item(rel->rd_support,
-					   relform->relnatts * (rel->rd_indam->amsupport * sizeof(RegProcedure)),
+					   nkeyatts * (rel->rd_indam->amsupport * sizeof(RegProcedure)),
 					   fp);
 
 			/* write the vector of collation OIDs */
 			write_item(rel->rd_indcollation,
-					   relform->relnatts * sizeof(Oid),
+					   nkeyatts * sizeof(Oid),
 					   fp);
 
 			/* write the vector of indoption values */
 			write_item(rel->rd_indoption,
-					   relform->relnatts * sizeof(int16),
+					   nkeyatts * sizeof(int16),
 					   fp);
-
-			Assert(rel->rd_opcoptions);
-
-			/* write the vector of opcoptions values */
-			for (i = 0; i < relform->relnatts; i++)
-			{
-				bytea	   *opt = rel->rd_opcoptions[i];
-
-				write_item(opt, opt ? VARSIZE(opt) : 0, fp);
-			}
 		}
 	}
 
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index b2fd6dbb73d..07adecc6e87 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -2027,19 +2027,111 @@ get_fn_expr_variadic(FmgrInfo *flinfo)
 }
 
 /*
- * Set options to FmgrInfo of opclass support function.
+ * Set options of an uncached FmgrInfo of opclass support function.
  *
  * Opclass support functions are called outside of expressions.  Thanks to that
  * we can use fn_expr to store opclass options as bytea constant.
+ *
+ * The relcache code will populate a relation's rd_supportinfo through
+ * set_fn_opclass_options_bulk().  This function is therefore mostly useful
+ * for non-default (e.g. cross-type) support procedures.
  */
 void
-set_fn_opclass_options(FmgrInfo *flinfo, bytea *options)
+set_fn_opclass_options(FmgrInfo *flinfo, const bytea *options)
 {
+	Assert(flinfo->fn_expr == NULL);
 	flinfo->fn_expr = (Node *) makeConst(BYTEAOID, -1, InvalidOid, -1,
 										 PointerGetDatum(options),
 										 options == NULL, false);
 }
 
+/* used only in set/clear_fn_opclass_options_bulk */
+const static Const OPCOPTION_NULL_CONST = {
+	.xpr = { .type = T_Const },
+	.consttype = BYTEAOID,
+	.consttypmod = -1,
+	.constcollid = InvalidOid,
+	.constlen = -1,
+	.constvalue = (Datum) (uintptr_t) NULL,
+	.constisnull = true,
+	.constbyval = false,
+	.location = -1
+};
+
+/*
+ * Set options to FmgrInfos of opclass support functions.
+ *
+ * Opclass support functions are called outside of expressions, and so don't
+ * use the fn_expr field.  As such, we use the field to store the index'
+ * opclass options inside a bytea-typed Const node.
+ *
+ * Note: For NULL opcopts, a sentinel OPCOPTION_NULL_CONST is used, all other
+ * options get their own Const allocation in the caller's memory context.
+ */
+void
+set_fn_opclass_options_bulk(FmgrInfo *flinfo, const bytea *const *opcopts, int nsupport, int nkeycols)
+{
+	if (nsupport == 0)
+		return;
+
+	Assert(flinfo != NULL);
+	Assert(opcopts != NULL);
+
+	for (int attno = 0; attno < nkeycols; attno++)
+	{
+		Node *option;
+		const bytea *opcoption = opcopts[attno];
+
+		/* Avoid allocating memory in the common case of no opcoptions */
+		if (opcoption == NULL)
+			option = (Node *) &OPCOPTION_NULL_CONST;
+		else
+		{
+			option = (Node *) makeConst(BYTEAOID, -1, InvalidOid, -1,
+										PointerGetDatum(opcoption),
+										false, false);
+		}
+		Assert(option != NULL);
+
+		/*
+		 * Put the Const container in the relevant support structs.
+		 *
+		 * We push the same allocation into all support functions of the same
+		 * attribute, so we can avoid what would be duplicate allocations, and
+		 * minimize duplicated allocations.
+		 */
+		for (int support = 0; support < nsupport; support++)
+		{
+			Assert(flinfo->fn_expr == NULL);
+
+			flinfo->fn_expr = option;
+			flinfo++;
+		}
+	}
+}
+
+/*
+ * Clear the opcopts from FmgrInfos of opclass support functions.
+ *
+* Opclass support functions are called outside of expressions, and so don't
+ * use the fn_expr field.  As such, we use the field to store the index'
+ * opclass options inside a bytea-typed Const node.
+ */
+void
+clear_fn_opclass_options_bulk(FmgrInfo *flinfo, int ninfos, int nkeyatts)
+{
+	int stride = ninfos / nkeyatts;
+	Assert(ninfos % nkeyatts == 0);
+
+	for (int i = 0; i < ninfos; i += stride)
+	{
+		/* avoid trying to free the sentinel for NULL opcinfos */
+		if (i % stride == 0 && flinfo[i].fn_expr != (Node *) &OPCOPTION_NULL_CONST)
+			pfree(flinfo[i].fn_expr);
+		flinfo[i].fn_expr = NULL;
+	}
+}
+
 /*
  * Check if options are defined for opclass support function.
  */
diff --git a/src/include/c.h b/src/include/c.h
index 20cfbac54e7..c49806bf447 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -1373,6 +1373,11 @@ typedef struct PGAlignedXLogBlock PGAlignedXLogBlock;
 #define unconstify(underlying_type, expr) \
 	(StaticAssertVariableIsOfTypeMacro(expr, const underlying_type), \
 	 (underlying_type) (expr))
+#define unconstify_array(underlying_type, expr) \
+	(StaticAssertExpr(__builtin_types_compatible_p(__typeof(expr), underlying_type const*), \
+					  "wrong cast"), \
+	 (underlying_type*) (expr))
+
 #define unvolatize(underlying_type, expr) \
 	(StaticAssertVariableIsOfTypeMacro(expr, volatile underlying_type), \
 	 (underlying_type) (expr))
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 04b7914095f..a7679daca28 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -779,7 +779,12 @@ extern bool get_call_expr_arg_stable(Node *expr, int argnum);
 extern bool get_fn_expr_variadic(FmgrInfo *flinfo);
 extern bytea *get_fn_opclass_options(FmgrInfo *flinfo);
 extern bool has_fn_opclass_options(FmgrInfo *flinfo);
-extern void set_fn_opclass_options(FmgrInfo *flinfo, bytea *options);
+extern void set_fn_opclass_options(FmgrInfo *flinfo, const bytea *options);
+extern void set_fn_opclass_options_bulk(FmgrInfo *flinfo,
+										const bytea * const*opcopts,
+										int ninfos, int nkeycols);
+extern void clear_fn_opclass_options_bulk(FmgrInfo *flinfo, int ninfos,
+										  int nkeyatts);
 extern bool CheckFunctionValidatorAccess(Oid validatorOid, Oid functionOid);
 
 /*
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c48e656ce80..37a8eb0043e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1392,7 +1392,7 @@ typedef struct IndexOptInfo
 	/* do NULLs come first in the sort order? or NULL if partitioned index */
 	bool	   *nulls_first pg_node_attr(array_size(nkeycolumns));
 	/* opclass-specific options for columns */
-	bytea	  **opclassoptions pg_node_attr(read_write_ignore);
+	const bytea *const *opclassoptions pg_node_attr(read_write_ignore);
 	/* which index cols can be returned in an index-only scan? */
 	bool	   *canreturn pg_node_attr(array_size(ncolumns));
 	/* OID of the access method (in pg_am) */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 78e052bf34b..574e1ca9d2a 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -215,7 +215,7 @@ typedef struct RelationData
 	Oid		   *rd_exclprocs;	/* OIDs of exclusion ops' procs, if any */
 	uint16	   *rd_exclstrats;	/* exclusion ops' strategy numbers, if any */
 	Oid		   *rd_indcollation;	/* OIDs of index collations */
-	bytea	  **rd_opcoptions;	/* parsed opclass-specific options */
+	const bytea *const *rd_opcoptions;	/* parsed opclass-specific options */
 
 	/*
 	 * rd_amcache is available for index and table AMs to cache private data
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 89c27aa1529..b1af1a2ec3f 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -60,7 +60,7 @@ extern Oid	RelationGetReplicaIndex(Relation relation);
 extern List *RelationGetIndexExpressions(Relation relation);
 extern List *RelationGetDummyIndexExpressions(Relation relation);
 extern List *RelationGetIndexPredicate(Relation relation);
-extern bytea **RelationGetIndexAttOptions(Relation relation, bool copy);
+extern const bytea *const *RelationGetIndexAttOptions(Relation relation, bool copy);
 
 /*
  * Which set of columns to return by RelationGetIndexAttrBitmap.
-- 
2.50.1 (Apple Git-155)

