From 668971fccba2c90d02236193bd3c98433b13487b Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Wed, 29 Jul 2026 19:45:33 +0200
Subject: [PATCH v3] Track whole-row Var dependencies in pg_depend

This enables correct index and constraint validation when a table
changes shape through DDL; and with it when the whole-row Var's
value changes.  Without this precise tracking, we'd have to scan
all constraints and indexes for invalidations every time a relevant
DDL is applied, which would make this prohibitively expensive.

Tests were added to make sure these changes correctly rebuild indexes.
---
 src/backend/catalog/dependency.c       |  42 +++--
 src/backend/catalog/objectaddress.c    |  24 ++-
 src/backend/commands/tablecmds.c       | 228 ++++++-------------------
 src/include/catalog/dependency.h       |   9 +
 src/test/regress/expected/indexing.out |  14 ++
 src/test/regress/sql/indexing.sql      |   9 +
 6 files changed, 129 insertions(+), 197 deletions(-)

diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 55ee42f8dfc..550e62dae95 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1648,10 +1648,12 @@ collectDependenciesOfExpr(ObjectAddresses *addrs,
  * to depend on the table not vice versa.
  *
  * NOTE: the caller should ensure that a whole-table dependency on the
- * specified relation is created separately, if one is needed.  In particular,
- * a whole-row Var "relation.*" will not cause this routine to emit any
- * dependency item.  This is appropriate behavior for subexpressions of an
- * ordinary query, so other cases need to cope as necessary.
+ * specified relation is created separately, if one is needed.
+ * In particular, a whole-row Var "relation.*" will cause this routine
+ * to emit a dependency on the WholeRowAttrNumber subobject, not the
+ * whole object (subobj 0).
+ * This is appropriate behavior for expressions in ordinary indexes, so
+ * other cases need to cope as necessary.
  */
 void
 recordDependencyOnSingleRelExpr(const ObjectAddress *depender,
@@ -1840,6 +1842,7 @@ find_expr_references_walker(Node *node,
 	if (IsA(node, Var))
 	{
 		Var		   *var = (Var *) node;
+		AttrNumber	attno;
 		List	   *rtable;
 		RangeTblEntry *rte;
 
@@ -1852,29 +1855,29 @@ find_expr_references_walker(Node *node,
 		rte = rt_fetch(var->varno, rtable);
 
 		/*
-		 * A whole-row Var references no specific columns, so adds no new
-		 * dependency.  (We assume that there is a whole-table dependency
-		 * arising from each underlying rangetable entry.  While we could
-		 * record such a dependency when finding a whole-row Var that
-		 * references a relation directly, it's quite unclear how to extend
-		 * that to whole-row Vars for JOINs, so it seems better to leave the
-		 * responsibility with the range table.  Note that this poses some
-		 * risks for identifying dependencies of stand-alone expressions:
-		 * whole-table references may need to be created separately.)
+		 * A whole-row Var references no specific columns.  We could assume
+		 * that there is a whole-table dependency arising from each
+		 * underlying rangetable entry, and in doing so ignore the
+		 * whole-row var, but doing so would make it difficult to find the
+		 * objects that contain whole-row Vars which need to be checked,
+		 * invalidated, or rebuilt when the referenced table changes shape.
 		 */
 		if (var->varattno == InvalidAttrNumber)
-			return false;
+			attno = WholeRowAttrNumber;
+		else
+			attno = var->varattno;
+
 		if (rte->rtekind == RTE_RELATION)
 		{
 			/* If it's a plain relation, reference this column */
-			add_object_address(RelationRelationId, rte->relid, var->varattno,
+			add_object_address(RelationRelationId, rte->relid, attno,
 							   context->addrs);
 		}
 		else if (rte->rtekind == RTE_FUNCTION)
 		{
 			/* Might need to add a dependency on a composite type's column */
 			/* (done out of line, because it's a bit bulky) */
-			process_function_rte_ref(rte, var->varattno, context);
+			process_function_rte_ref(rte, attno, context);
 		}
 
 		/*
@@ -2552,6 +2555,13 @@ process_function_rte_ref(RangeTblEntry *rte, AttrNumber attnum,
 	if (rte->funcordinality && attnum == atts_done + 1)
 		return;
 
+	/*
+	 * Signatures of functions and procedures are immutable, so no need to
+	 * track its dependents.
+	 */
+	if (attnum == WholeRowAttrNumber)
+		return;
+
 	/* this probably can't happen ... */
 	ereport(ERROR,
 			(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 703754a8123..64e9d3fa15b 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -3018,6 +3018,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 		case RelationRelationId:
 			if (object->objectSubId == 0)
 				getRelationDescription(&buffer, object->objectId, missing_ok);
+			else if (object->objectSubId == WholeRowAttrNumber)
+			{
+				StringInfoData rel;
+
+				initStringInfo(&rel);
+				getRelationDescription(&rel, object->objectId, missing_ok);
+				/* translator: %s is, e.g., "table %s" */
+				appendStringInfo(&buffer, _("whole row of %s"), rel.data);
+				pfree(rel.data);
+			}
 			else
 			{
 				/* column, not whole relation */
@@ -5009,8 +5019,14 @@ getRelationTypeDescription(StringInfo buffer, Oid relid, int32 objectSubId,
 			break;
 	}
 
-	if (objectSubId != 0)
+	if (objectSubId == WholeRowAttrNumber)
+	{
+		appendStringInfoString(buffer, " whole row");
+	}
+	else if (objectSubId != 0)
+	{
 		appendStringInfoString(buffer, " column");
+	}
 
 	ReleaseSysCache(relTup);
 }
@@ -5139,7 +5155,7 @@ getObjectIdentityParts(const ObjectAddress *object,
 				 * Check for the attribute first, so as if it is missing we
 				 * can skip the entire relation description.
 				 */
-				if (object->objectSubId != 0)
+				if (object->objectSubId != 0 && object->objectSubId != WholeRowAttrNumber)
 				{
 					attr = get_attname(object->objectId,
 									   object->objectSubId,
@@ -5161,6 +5177,10 @@ getObjectIdentityParts(const ObjectAddress *object,
 					if (objname)
 						*objname = lappend(*objname, attr);
 				}
+				else if (object->objectSubId == WholeRowAttrNumber)
+				{
+					appendStringInfo(&buffer, ".*");
+				}
 			}
 			break;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fd144d783d9..d7b57785ed0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -556,7 +556,8 @@ static ObjectAddress ATExecSetStorage(Relation rel, const char *colName,
 static void ATPrepDropColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
 							 AlterTableCmd *cmd, LOCKMODE lockmode,
 							 AlterTableUtilityContext *context);
-static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
+static ObjectAddress ATExecDropColumn(List **wqueue, AlteredTableInfo *tab,
+									  Relation rel, const char *colName,
 									  DropBehavior behavior,
 									  bool recurse, bool recursing,
 									  bool missing_ok, LOCKMODE lockmode,
@@ -5409,7 +5410,8 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
 			 * (this is not done in ATExecAlterColumnType since it should be
 			 * done only once if multiple columns of a table are altered).
 			 */
-			if (pass == AT_PASS_ALTER_TYPE || pass == AT_PASS_SET_EXPRESSION)
+			if (pass == AT_PASS_ADD_COL || pass == AT_PASS_DROP ||
+				pass == AT_PASS_ALTER_TYPE || pass == AT_PASS_SET_EXPRESSION)
 				ATPostAlterTypeCleanup(wqueue, tab, lockmode);
 
 			if (tab->rel)
@@ -5508,7 +5510,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 										   lockmode);
 			break;
 		case AT_DropColumn:		/* DROP COLUMN */
-			address = ATExecDropColumn(wqueue, rel, cmd->name,
+			address = ATExecDropColumn(wqueue, tab, rel, cmd->name,
 									   cmd->behavior, cmd->recurse, false,
 									   cmd->missing_ok, lockmode,
 									   NULL);
@@ -7444,6 +7446,12 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("cannot recursively add identity column to table that has child tables")));
 
+	/*
+	 * Remember we need to invalidate whole-row dependents, to make sure they
+	 * correctly handle the new column.
+	 */
+	RememberWholeRowDependentForRebuilding(tab, AT_AddColumn, rel);
+
 	pgclass = table_open(RelationRelationId, RowExclusiveLock);
 
 	reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(myrelid));
@@ -9362,11 +9370,10 @@ ATPrepDropColumn(List **wqueue, Relation rel, bool recurse, bool recursing,
  * checked recursively.
  */
 static ObjectAddress
-ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
-				 DropBehavior behavior,
-				 bool recurse, bool recursing,
-				 bool missing_ok, LOCKMODE lockmode,
-				 ObjectAddresses *addrs)
+ATExecDropColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
+				 const char *colName, DropBehavior behavior,
+				 bool recurse, bool recursing, bool missing_ok,
+				 LOCKMODE lockmode, ObjectAddresses *addrs)
 {
 	HeapTuple	tuple;
 	Form_pg_attribute targetatt;
@@ -9446,6 +9453,9 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/* Invalidate whole-row */
+	RememberWholeRowDependentForRebuilding(tab, AT_DropColumn, rel);
+
 	/*
 	 * Propagate to children as appropriate.  Unlike most other ALTER
 	 * routines, we have to do this one level of recursion at a time; we can't
@@ -9500,9 +9510,9 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 				if (childatt->attinhcount == 1 && !childatt->attislocal)
 				{
 					/* Time to delete this child column, too */
-					ATExecDropColumn(wqueue, childrel, colName,
-									 behavior, true, true,
-									 false, lockmode, addrs);
+					ATExecDropColumn(wqueue, tab, childrel,
+									 colName, behavior, true,
+									 true, false, lockmode, addrs);
 				}
 				else
 				{
@@ -15326,6 +15336,13 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	 */
 	RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName);
 
+	/*
+	 * Find whole-row referenced objects that depend on the column
+	 * (constraints, indexes, etc.), and record enough information to let us
+	 * recreate the objects.
+	 */
+	RememberWholeRowDependentForRebuilding(tab, AT_AlterColumnType, rel);
+
 	/*
 	 * Now scan for dependencies of this column on other things.  The only
 	 * things we should find are the dependency on the column datatype and
@@ -15521,8 +15538,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 }
 
 /*
- * Subroutine for ATExecAlterColumnType and ATExecSetExpression: Find everything
- * that depends on the column (constraints, indexes, etc), and record enough
+ * Subroutine for ATExecAlterColumnType, ATExecSetExpression, ATExecAddColumn,
+ * and ATExecDropColumn: Find everything that depends on the column or
+ * whole-row reference (constraints, indexes, etc), and record enough
  * information to let us recreate the objects.
  */
 static void
@@ -15534,7 +15552,9 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 	SysScanDesc scan;
 	HeapTuple	depTup;
 
-	Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression);
+	Assert(subtype == AT_AlterColumnType || subtype == AT_SetExpression ||
+		   (attnum == WholeRowAttrNumber && (subtype == AT_AddColumn ||
+											 subtype == AT_DropColumn)));
 
 	depRel = table_open(DependRelationId, RowExclusiveLock);
 
@@ -15608,8 +15628,10 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				 *
 				 * This is only a problem for AT_AlterColumnType, not
 				 * AT_SetExpression.
+				 *
+				 * Whole Row expressions are ignored.
 				 */
-				if (subtype == AT_AlterColumnType)
+				if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("cannot alter type of a column used by a function or procedure"),
@@ -15624,7 +15646,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				 * View/rule bodies have pretty much the same issues as
 				 * function bodies.  FIXME someday.
 				 */
-				if (subtype == AT_AlterColumnType)
+				if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("cannot alter type of a column used by a view or rule"),
@@ -15644,7 +15666,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				 * issues as above.  Since we can't easily tell which case
 				 * applies, we punt for both.  FIXME someday.
 				 */
-				if (subtype == AT_AlterColumnType)
+				if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("cannot alter type of a column used in a trigger definition"),
@@ -15663,7 +15685,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				 * easy enough to remove and recreate the policy; still, FIXME
 				 * someday.
 				 */
-				if (subtype == AT_AlterColumnType)
+				if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("cannot alter type of a column used in a policy definition"),
@@ -15694,7 +15716,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 						 * by SQL standard, so just punt for now.  It might be
 						 * doable with some thinking and effort.
 						 */
-						if (subtype == AT_AlterColumnType)
+						if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 							ereport(ERROR,
 									(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 									 errmsg("cannot alter type of a column used by a generated column"),
@@ -15722,7 +15744,7 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				 * Column reference in a PUBLICATION ... FOR TABLE ... WHERE
 				 * clause.  Same issues as above.  FIXME someday.
 				 */
-				if (subtype == AT_AlterColumnType)
+				if (subtype == AT_AlterColumnType && attnum != WholeRowAttrNumber)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("cannot alter type of a column used by a publication WHERE clause"),
@@ -15759,160 +15781,8 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 static void
 RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel)
 {
-	ScanKeyData skey;
-	Relation	pg_constraint;
-	Relation	pg_index;
-	SysScanDesc conscan;
-	SysScanDesc indscan;
-	HeapTuple	constrTuple;
-	HeapTuple	indexTuple;
-	bool		isnull;
-
-	Assert(subtype == AT_SetExpression);
-
-	/*
-	 * Check CHECK constraints with whole-row references first.
-	 */
-	if (RelationGetDescr(rel)->constr &&
-		RelationGetDescr(rel)->constr->num_check > 0)
-	{
-		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
-
-		ScanKeyInit(&skey,
-					Anum_pg_constraint_conrelid,
-					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(RelationGetRelid(rel)));
-
-		conscan = systable_beginscan(pg_constraint,
-									 ConstraintRelidTypidNameIndexId,
-									 true,
-									 NULL,
-									 1,
-									 &skey);
-
-		while (HeapTupleIsValid(constrTuple = systable_getnext(conscan)))
-		{
-			Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(constrTuple);
-			Datum		exprDatum;
-
-			if (conform->contype != CONSTRAINT_CHECK)
-				continue;
-
-			exprDatum = heap_getattr(constrTuple,
-									 Anum_pg_constraint_conbin,
-									 RelationGetDescr(pg_constraint),
-									 &isnull);
-			if (isnull)
-				elog(ERROR, "null conbin for relation \"%s\"",
-					 RelationGetRelationName(rel));
-			else
-			{
-				char	   *exprString;
-				Node	   *expr;
-				Bitmapset  *expr_attrs = NULL;
-
-				exprString = TextDatumGetCString(exprDatum);
-				expr = stringToNode(exprString);
-				pfree(exprString);
-
-				/* Find all attributes referenced */
-				pull_varattnos(expr, 1, &expr_attrs);
-
-				/*
-				 * If the CHECK constraint contains whole-row reference then
-				 * remember it.
-				 */
-				if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
-				{
-					RememberConstraintForRebuilding(conform->oid, tab);
-				}
-			}
-		}
-		systable_endscan(conscan);
-		table_close(pg_constraint, AccessShareLock);
-	}
-
-	/*
-	 * Now check indexes with whole-row references. Prepare to scan pg_index
-	 * for entries having indrelid matching this relation.
-	 */
-	ScanKeyInit(&skey,
-				Anum_pg_index_indrelid,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(RelationGetRelid(rel)));
-
-	pg_index = table_open(IndexRelationId, AccessShareLock);
-
-	indscan = systable_beginscan(pg_index,
-								 IndexIndrelidIndexId,
-								 true,
-								 NULL,
-								 1,
-								 &skey);
-
-	while (HeapTupleIsValid(indexTuple = systable_getnext(indscan)))
-	{
-		Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple);
-		Datum		exprDatum;
-
-		exprDatum = heap_getattr(indexTuple,
-								 Anum_pg_index_indexprs,
-								 RelationGetDescr(pg_index),
-								 &isnull);
-		if (!isnull)
-		{
-			char	   *exprString;
-			Node	   *expr;
-			Bitmapset  *expr_attrs = NULL;
-
-			exprString = TextDatumGetCString(exprDatum);
-			expr = stringToNode(exprString);
-			pfree(exprString);
-
-			/* Find all attributes referenced */
-			pull_varattnos(expr, 1, &expr_attrs);
-
-			/*
-			 * If the index expression contains a whole-row reference then
-			 * remember it.
-			 */
-			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
-			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
-				continue;
-			}
-		}
-
-		exprDatum = heap_getattr(indexTuple,
-								 Anum_pg_index_indpred,
-								 RelationGetDescr(pg_index),
-								 &isnull);
-		if (!isnull)
-		{
-			char	   *exprString;
-			Node	   *expr;
-			Bitmapset  *expr_attrs = NULL;
-
-			exprString = TextDatumGetCString(exprDatum);
-			expr = stringToNode(exprString);
-			pfree(exprString);
-
-			/* Find all attributes referenced */
-			pull_varattnos(expr, 1, &expr_attrs);
-
-			/*
-			 * If the index predicate expression contains a whole-row
-			 * reference then remember it.
-			 */
-			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
-			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
-			}
-		}
-	}
-
-	systable_endscan(indscan);
-	table_close(pg_index, AccessShareLock);
+	return RememberAllDependentForRebuilding(tab, subtype, rel,
+											 WholeRowAttrNumber, "*");
 }
 
 /*
@@ -16094,11 +15964,11 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 }
 
 /*
- * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION
- * operations for a particular relation.  We have to drop and recreate all the
- * indexes and constraints that depend on the altered columns.  We do the
- * actual dropping here, but re-creation is managed by adding work queue
- * entries to do those steps later.
+ * Cleanup after we've finished all the ADD COLUMN, DROP COLUMN, ALTER TYPE,
+ * or SET EXPRESSION operations for a particular relation.  We have to drop
+ * and recreate all the indexes and constraints that depend on the altered
+ * columns.  We do the actual dropping here, but re-creation is managed by
+ * adding work queue entries to do those steps later.
  */
 static void
 ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 7da6a3942e1..0675e79d0c4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -97,6 +97,15 @@ typedef struct ObjectAddresses ObjectAddresses;
 #define PERFORM_DELETION_CONCURRENT_LOCK	0x0020	/* normal drop with
 													 * concurrent lock mode */
 
+/*
+ * Attribute number used to track whole-row references, to allow
+ * tracking of dependencies on the shape of the referenced relation.
+ *
+ * Without this, we'd have to scan all dependent objects' expressions
+ * every time a column is added, dropped, or modified.
+ */
+#define WholeRowAttrNumber		INT16_MIN
+
 
 /* in dependency.c */
 
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index 4a0a652e9f6..b89f3bfc094 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -1782,6 +1782,20 @@ insert into test_pg_wholerow_index values (1, 'multiplication', 1.0);
 create index row_image_index
     on test_pg_wholerow_index ((row_image(test_pg_wholerow_index)));
 insert into test_pg_wholerow_index values (2, 'addition', 0);
+-- test shape changes invalidate an index with whole-row references
+select relfilenode as rfn_orig from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index add column d int; -- add column
+select relfilenode as rfn_ac from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index alter column d set data type bigint; -- alter column type
+select relfilenode as rfn_sdt from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index drop column d; -- drop column
+-- Check that the relfilenode changed at every step
+select :rfn_orig != :rfn_ac AND :rfn_ac != :rfn_sdt AND :rfn_sdt != relfilenode from pg_class where oid = 'row_image_index'::regclass;
+ ?column? 
+----------
+ t
+(1 row)
+
 drop index row_image_index;
 drop function row_image(test_pg_wholerow_index);
 drop table test_pg_wholerow_index;
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index bbcfb365281..ec9513c6fb7 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -1002,6 +1002,15 @@ insert into test_pg_wholerow_index values (1, 'multiplication', 1.0);
 create index row_image_index
     on test_pg_wholerow_index ((row_image(test_pg_wholerow_index)));
 insert into test_pg_wholerow_index values (2, 'addition', 0);
+-- test shape changes invalidate an index with whole-row references
+select relfilenode as rfn_orig from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index add column d int; -- add column
+select relfilenode as rfn_ac from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index alter column d set data type bigint; -- alter column type
+select relfilenode as rfn_sdt from pg_class where oid = 'row_image_index'::regclass \gset
+alter table test_pg_wholerow_index drop column d; -- drop column
+-- Check that the relfilenode changed at every step
+select :rfn_orig != :rfn_ac AND :rfn_ac != :rfn_sdt AND :rfn_sdt != relfilenode from pg_class where oid = 'row_image_index'::regclass;
 drop index row_image_index;
 drop function row_image(test_pg_wholerow_index);
 drop table test_pg_wholerow_index;
-- 
2.50.1 (Apple Git-155)

