From 62d51356968e403bde019d563c5b581bdb6a9903 Mon Sep 17 00:00:00 2001
From: Zizhuan Liu <44973863@qq.com>
Date: Tue, 21 Jul 2026 23:04:50 +0800
Subject: [PATCH v8] V8 enhance-index-support-for-virtual-generated-columns

---
 src/backend/access/heap/heapam_handler.c      |  20 +-
 src/backend/bootstrap/bootstrap.c             |   6 +
 src/backend/catalog/index.c                   |  62 ++++-
 src/backend/catalog/indexing.c                |   2 +
 src/backend/catalog/toasting.c                |   4 +
 src/backend/commands/analyze.c                |  10 +-
 src/backend/commands/indexcmds.c              |  49 ++--
 src/backend/executor/execIndexing.c           |  13 +-
 src/backend/nodes/makefuncs.c                 |  10 +-
 src/backend/optimizer/path/costsize.c         |   6 +
 src/backend/optimizer/path/indxpath.c         |  22 +-
 src/backend/optimizer/plan/createplan.c       |   6 +-
 src/backend/optimizer/util/plancat.c          |  22 +-
 src/backend/utils/adt/selfuncs.c              |  16 +-
 src/backend/utils/cache/relcache.c            | 105 ++++++++
 src/include/nodes/execnodes.h                 |   4 +
 src/include/nodes/pathnodes.h                 |   2 +
 src/include/utils/rel.h                       |   2 +
 src/include/utils/relcache.h                  |   4 +
 .../regress/expected/generated_virtual.out    | 233 +++++++++++++++---
 src/test/regress/expected/indexing.out        |  33 +++
 src/test/regress/sql/generated_virtual.sql    |  88 ++++---
 src/test/regress/sql/indexing.sql             |  12 +
 23 files changed, 582 insertions(+), 149 deletions(-)

diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2268cc27..22e0762e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -1159,7 +1159,7 @@ heapam_index_build_range_scan(Relation heapRelation,
 	Datum		values[INDEX_MAX_KEYS];
 	bool		isnull[INDEX_MAX_KEYS];
 	double		reltuples;
-	ExprState  *predicate;
+	ExprState  *predicateExpand;
 	TupleTableSlot *slot;
 	EState	   *estate;
 	ExprContext *econtext;
@@ -1200,7 +1200,7 @@ heapam_index_build_range_scan(Relation heapRelation,
 	econtext->ecxt_scantuple = slot;
 
 	/* Set up execution state for predicate, if any. */
-	predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+	predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 
 	/*
 	 * Prepare for scan of the base relation.  In a normal index build, we use
@@ -1607,9 +1607,9 @@ heapam_index_build_range_scan(Relation heapRelation,
 		 * In a partial index, discard tuples that don't satisfy the
 		 * predicate.
 		 */
-		if (predicate != NULL)
+		if (predicateExpand != NULL)
 		{
-			if (!ExecQual(predicate, econtext))
+			if (!ExecQual(predicateExpand, econtext))
 				continue;
 		}
 
@@ -1709,7 +1709,9 @@ heapam_index_build_range_scan(Relation heapRelation,
 
 	/* These may have been pointing to the now-gone estate */
 	indexInfo->ii_ExpressionsState = NIL;
+	indexInfo->ii_ExpressionsExpandState = NIL;
 	indexInfo->ii_PredicateState = NULL;
+	indexInfo->ii_PredicateExpandState = NULL;
 
 	return reltuples;
 }
@@ -1726,7 +1728,7 @@ heapam_index_validate_scan(Relation heapRelation,
 	HeapTuple	heapTuple;
 	Datum		values[INDEX_MAX_KEYS];
 	bool		isnull[INDEX_MAX_KEYS];
-	ExprState  *predicate;
+	ExprState  *predicateExpand;
 	TupleTableSlot *slot;
 	EState	   *estate;
 	ExprContext *econtext;
@@ -1758,7 +1760,7 @@ heapam_index_validate_scan(Relation heapRelation,
 	econtext->ecxt_scantuple = slot;
 
 	/* Set up execution state for predicate, if any. */
-	predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+	predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 
 	/*
 	 * Prepare for scan of the base relation.  We need just those tuples
@@ -1895,9 +1897,9 @@ heapam_index_validate_scan(Relation heapRelation,
 			 * In a partial index, discard tuples that don't satisfy the
 			 * predicate.
 			 */
-			if (predicate != NULL)
+			if (predicateExpand != NULL)
 			{
-				if (!ExecQual(predicate, econtext))
+				if (!ExecQual(predicateExpand, econtext))
 					continue;
 			}
 
@@ -1952,7 +1954,9 @@ heapam_index_validate_scan(Relation heapRelation,
 
 	/* These may have been pointing to the now-gone estate */
 	indexInfo->ii_ExpressionsState = NIL;
+	indexInfo->ii_ExpressionsExpandState = NIL;
 	indexInfo->ii_PredicateState = NULL;
+	indexInfo->ii_PredicateExpandState = NULL;
 }
 
 /*
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index b0dcd987..80bf4fc4 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -1156,11 +1156,17 @@ index_register(Oid heap,
 	/* expressions will likely be null, but may as well copy it */
 	newind->il_info->ii_Expressions =
 		copyObject(indexInfo->ii_Expressions);
+	newind->il_info->ii_ExpressionsExpand =
+		copyObject(indexInfo->ii_ExpressionsExpand);
 	newind->il_info->ii_ExpressionsState = NIL;
+	newind->il_info->ii_ExpressionsExpandState = NIL;
 	/* predicate will likely be null, but may as well copy it */
 	newind->il_info->ii_Predicate =
 		copyObject(indexInfo->ii_Predicate);
+	newind->il_info->ii_PredicateExpand =
+		copyObject(indexInfo->ii_PredicateExpand);
 	newind->il_info->ii_PredicateState = NULL;
+	newind->il_info->ii_PredicateExpandState = NULL;
 	/* no exclusion constraints at bootstrap time, so no need to copy */
 	Assert(indexInfo->ii_ExclusionOps == NULL);
 	Assert(indexInfo->ii_ExclusionProcs == NULL);
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357..c4d64925 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1403,6 +1403,10 @@ index_create_copy(Relation heapRelation, uint16 flags,
 							concurrently,	/* concurrent */
 							indexRelation->rd_indam->amsummarizing,
 							oldInfo->ii_WithoutOverlaps);
+	newInfo->ii_ExpressionsExpand =
+		ExpandVirtualGeneratedColumns(newInfo->ii_ExpressionsExpand, heapRelation, InvalidOid);
+	newInfo->ii_PredicateExpand =
+		ExpandVirtualGeneratedColumns(newInfo->ii_PredicateExpand, heapRelation, InvalidOid);
 
 	/* fetch exclusion constraint info if any */
 	if (indexRelation->rd_index->indisexclusion)
@@ -2471,6 +2475,8 @@ BuildIndexInfo(Relation index)
 					   false,
 					   index->rd_indam->amsummarizing,
 					   indexStruct->indisexclusion && indexStruct->indisunique);
+	ii->ii_ExpressionsExpand = (List *)RelationGetIndexExpressionsExpand(index);
+	ii->ii_PredicateExpand = (List *)RelationGetIndexPredicateExpand(index);
 
 	/* fill in attribute numbers */
 	for (i = 0; i < numAtts; i++)
@@ -2760,10 +2766,12 @@ FormIndexDatum(IndexInfo *indexInfo,
 		/* First time through, set up expression evaluation state */
 		indexInfo->ii_ExpressionsState =
 			ExecPrepareExprList(indexInfo->ii_Expressions, estate);
+		indexInfo->ii_ExpressionsExpandState =
+			ExecPrepareExprList(indexInfo->ii_ExpressionsExpand, estate);
 		/* Check caller has set up context correctly */
 		Assert(GetPerTupleExprContext(estate)->ecxt_scantuple == slot);
 	}
-	indexpr_item = list_head(indexInfo->ii_ExpressionsState);
+	indexpr_item = list_head(indexInfo->ii_ExpressionsExpandState);
 
 	for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
 	{
@@ -2775,11 +2783,41 @@ FormIndexDatum(IndexInfo *indexInfo,
 			iDatum = slot_getsysattr(slot, keycol, &isNull);
 		else if (keycol != 0)
 		{
-			/*
-			 * Plain index column; get the value we need directly from the
-			 * heap tuple.
-			 */
-			iDatum = slot_getattr(slot, keycol, &isNull);
+			TupleDesc			tupdesc = slot->tts_tupleDescriptor;
+			Form_pg_attribute 	att = TupleDescAttr(tupdesc, keycol - 1);
+
+			if (att->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
+			{
+				/*
+				 * Plain index column; get the value we need directly from the
+				 * heap tuple.
+				*/
+				iDatum = slot_getattr(slot, keycol, &isNull);
+			}
+			else
+			{
+				TupleConstr 	*constr = tupdesc->constr;
+				int				 j;
+				AttrDefault		*defval;
+				Expr			*expr;
+				ExprState		*exprstate;
+
+				Assert(constr->num_defval > 0);
+				for (j = 0; j < constr->num_defval; j++)
+				{
+					defval = &constr->defval[j];
+					if (defval->adnum != keycol)
+						continue;
+
+					expr = stringToNode(defval->adbin);
+					exprstate = ExecPrepareExpr(expr, estate);
+					iDatum = ExecEvalExprSwitchContext(exprstate,
+														GetPerTupleExprContext(estate),
+														&isNull);
+					break;
+				}
+				Assert(j < constr->num_defval);
+			}
 		}
 		else
 		{
@@ -2791,7 +2829,7 @@ FormIndexDatum(IndexInfo *indexInfo,
 			iDatum = ExecEvalExprSwitchContext((ExprState *) lfirst(indexpr_item),
 											   GetPerTupleExprContext(estate),
 											   &isNull);
-			indexpr_item = lnext(indexInfo->ii_ExpressionsState, indexpr_item);
+			indexpr_item = lnext(indexInfo->ii_ExpressionsExpandState, indexpr_item);
 		}
 		values[i] = iDatum;
 		isnull[i] = isNull;
@@ -3220,7 +3258,7 @@ IndexCheckExclusion(Relation heapRelation,
 	TableScanDesc scan;
 	Datum		values[INDEX_MAX_KEYS];
 	bool		isnull[INDEX_MAX_KEYS];
-	ExprState  *predicate;
+	ExprState  *predicateExpand;
 	TupleTableSlot *slot;
 	EState	   *estate;
 	ExprContext *econtext;
@@ -3246,7 +3284,7 @@ IndexCheckExclusion(Relation heapRelation,
 	econtext->ecxt_scantuple = slot;
 
 	/* Set up execution state for predicate, if any. */
-	predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+	predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 
 	/*
 	 * Scan all live tuples in the base relation.
@@ -3266,9 +3304,9 @@ IndexCheckExclusion(Relation heapRelation,
 		/*
 		 * In a partial index, ignore tuples that don't satisfy the predicate.
 		 */
-		if (predicate != NULL)
+		if (predicateExpand != NULL)
 		{
-			if (!ExecQual(predicate, econtext))
+			if (!ExecQual(predicateExpand, econtext))
 				continue;
 		}
 
@@ -3301,7 +3339,9 @@ IndexCheckExclusion(Relation heapRelation,
 
 	/* These may have been pointing to the now-gone estate */
 	indexInfo->ii_ExpressionsState = NIL;
+	indexInfo->ii_ExpressionsExpandState = NIL;
 	indexInfo->ii_PredicateState = NULL;
+	indexInfo->ii_PredicateExpandState = NULL;
 }
 
 /*
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index fd7d2ec0..d27aaa09 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -133,7 +133,9 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple,
 		 * supported, nor exclusion constraints, nor deferred uniqueness
 		 */
 		Assert(indexInfo->ii_Expressions == NIL);
+		Assert(indexInfo->ii_ExpressionsExpand == NIL);
 		Assert(indexInfo->ii_Predicate == NIL);
+		Assert(indexInfo->ii_PredicateExpand == NIL);
 		Assert(indexInfo->ii_ExclusionOps == NULL);
 		Assert(index->rd_index->indimmediate);
 		Assert(indexInfo->ii_NumIndexKeyAttrs != 0);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4b..daebfda1 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -298,9 +298,13 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	indexInfo->ii_IndexAttrNumbers[0] = 1;
 	indexInfo->ii_IndexAttrNumbers[1] = 2;
 	indexInfo->ii_Expressions = NIL;
+	indexInfo->ii_ExpressionsExpand = NIL;
 	indexInfo->ii_ExpressionsState = NIL;
+	indexInfo->ii_ExpressionsExpandState = NIL;
 	indexInfo->ii_Predicate = NIL;
+	indexInfo->ii_PredicateExpand = NIL;
 	indexInfo->ii_PredicateState = NULL;
+	indexInfo->ii_PredicateExpandState = NULL;
 	indexInfo->ii_ExclusionOps = NULL;
 	indexInfo->ii_ExclusionProcs = NULL;
 	indexInfo->ii_ExclusionStrats = NULL;
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b7..fc67c24a 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -899,7 +899,7 @@ compute_index_stats(Relation onerel, double totalrows,
 		TupleTableSlot *slot;
 		EState	   *estate;
 		ExprContext *econtext;
-		ExprState  *predicate;
+		ExprState  *predicateExpand;
 		Datum	   *exprvals;
 		bool	   *exprnulls;
 		int			numindexrows,
@@ -908,7 +908,7 @@ compute_index_stats(Relation onerel, double totalrows,
 		double		totalindexrows;
 
 		/* Ignore index if no columns to analyze and not partial */
-		if (attr_cnt == 0 && indexInfo->ii_Predicate == NIL)
+		if (attr_cnt == 0 && indexInfo->ii_PredicateExpand == NIL)
 			continue;
 
 		/*
@@ -926,7 +926,7 @@ compute_index_stats(Relation onerel, double totalrows,
 		econtext->ecxt_scantuple = slot;
 
 		/* Set up execution state for predicate. */
-		predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+		predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 
 		/* Compute and save index expression values */
 		exprvals = (Datum *) palloc(numrows * attr_cnt * sizeof(Datum));
@@ -949,9 +949,9 @@ compute_index_stats(Relation onerel, double totalrows,
 			ExecStoreHeapTuple(heapTuple, slot, false);
 
 			/* If index is partial, check predicate */
-			if (predicate != NULL)
+			if (predicateExpand != NULL)
 			{
-				if (!ExecQual(predicate, econtext))
+				if (!ExecQual(predicateExpand, econtext))
 					continue;
 			}
 			numindexrows++;
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 9ab74c8d..e3afdfad 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -932,6 +932,8 @@ DefineIndex(ParseState *pstate,
 							  concurrent,
 							  amissummarizing,
 							  stmt->iswithoutoverlaps);
+	indexInfo->ii_PredicateExpand =
+		ExpandVirtualGeneratedColumns(indexInfo->ii_PredicateExpand, rel, InvalidOid);
 
 	typeIds = palloc_array(Oid, numberOfAttributes);
 	collationIds = palloc_array(Oid, numberOfAttributes);
@@ -948,6 +950,8 @@ DefineIndex(ParseState *pstate,
 					  root_save_userid, root_save_sec_context,
 					  &root_save_nestlevel);
 
+	indexInfo->ii_ExpressionsExpand =
+		ExpandVirtualGeneratedColumns(indexInfo->ii_ExpressionsExpand, rel, InvalidOid);
 	/*
 	 * Extra checks when creating a PRIMARY KEY index.
 	 */
@@ -1110,7 +1114,7 @@ DefineIndex(ParseState *pstate,
 	 * We disallow indexes on system columns.  They would not necessarily get
 	 * updated correctly, and they don't seem useful anyway.
 	 *
-	 * Also disallow virtual generated columns in indexes (use expression
+	 * Aisallow virtual generated columns in indexes (include using expression
 	 * index instead).
 	 */
 	for (int i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
@@ -1121,27 +1125,14 @@ DefineIndex(ParseState *pstate,
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("index creation on system columns is not supported")));
-
-
-		if (attno > 0 &&
-			TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					stmt->primary ?
-					errmsg("primary keys on virtual generated columns are not supported") :
-					stmt->isconstraint ?
-					errmsg("unique constraints on virtual generated columns are not supported") :
-					errmsg("indexes on virtual generated columns are not supported"));
 	}
 
 	/*
-	 * Also check for system and generated columns used in expressions or
-	 * predicates.
+	 * Also check for system columns used in expressions or predicates.
 	 */
 	if (indexInfo->ii_Expressions || indexInfo->ii_Predicate)
 	{
 		Bitmapset  *indexattrs = NULL;
-		int			j;
 
 		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
 		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -1154,25 +1145,6 @@ DefineIndex(ParseState *pstate,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("index creation on system columns is not supported")));
 		}
-
-		/*
-		 * XXX Virtual generated columns in index expressions or predicates
-		 * could be supported, but it needs support in
-		 * RelationGetIndexExpressions() and RelationGetIndexPredicate().
-		 */
-		j = -1;
-		while ((j = bms_next_member(indexattrs, j)) >= 0)
-		{
-			AttrNumber	attno = j + FirstLowInvalidHeapAttributeNumber;
-
-			if (attno > 0 &&
-				TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 stmt->isconstraint ?
-						 errmsg("unique constraints on virtual generated columns are not supported") :
-						 errmsg("indexes on virtual generated columns are not supported")));
-		}
 	}
 
 	/* Is index safe for others to ignore?  See set_indexsafe_procflags() */
@@ -1876,6 +1848,11 @@ CheckPredicate(Expr *predicate)
  * If the caller switched to the table owner, ddl_userid is the role for ACL
  * checks reached without traversing opaque expressions.  Otherwise, it's
  * InvalidOid, and other ddl_* arguments are undefined.
+ *
+ * Upon returning from this function, callers must apply
+ * ExpandVirtualGeneratedColumns() to ii_ExpressionsExpand
+ * when necessary for actual expansion if ii_ExpressionsExpand
+ * is not NIL or dummy.
  */
 static void
 ComputeIndexAttrs(ParseState *pstate,
@@ -2274,6 +2251,10 @@ ComputeIndexAttrs(ParseState *pstate,
 
 		attn++;
 	}
+
+	indexInfo->ii_ExpressionsExpand =
+		ExpandVirtualGeneratedColumns(copyObject(indexInfo->ii_Expressions),
+											  NULL, relId);
 }
 
 /*
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index eb383812..b47daaf5 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -380,20 +380,24 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
 		if (indexInfo->ii_Predicate != NIL)
 		{
 			ExprState  *predicate;
+			ExprState  *predicateExpand;
 
 			/*
 			 * If predicate state not set up yet, create it (in the estate's
 			 * per-query context)
 			 */
 			predicate = indexInfo->ii_PredicateState;
+			predicateExpand = indexInfo->ii_PredicateExpandState;
 			if (predicate == NULL)
 			{
 				predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+				predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 				indexInfo->ii_PredicateState = predicate;
+				indexInfo->ii_PredicateExpandState = predicateExpand;
 			}
 
 			/* Skip this index-update if the predicate isn't satisfied */
-			if (!ExecQual(predicate, econtext))
+			if (!ExecQual(predicateExpand, econtext))
 				continue;
 		}
 
@@ -616,20 +620,24 @@ ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
 		if (indexInfo->ii_Predicate != NIL)
 		{
 			ExprState  *predicate;
+			ExprState  *predicateExpand;
 
 			/*
 			 * If predicate state not set up yet, create it (in the estate's
 			 * per-query context)
 			 */
 			predicate = indexInfo->ii_PredicateState;
+			predicateExpand = indexInfo->ii_PredicateExpandState;
 			if (predicate == NULL)
 			{
 				predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+				predicateExpand = ExecPrepareQual(indexInfo->ii_PredicateExpand, estate);
 				indexInfo->ii_PredicateState = predicate;
+				indexInfo->ii_PredicateExpandState = predicateExpand;
 			}
 
 			/* Skip this index-update if the predicate isn't satisfied */
-			if (!ExecQual(predicate, econtext))
+			if (!ExecQual(predicateExpand, econtext))
 				continue;
 		}
 
@@ -1102,6 +1110,7 @@ index_unchanged_by_update(ResultRelInfo *resultRelInfo, EState *estate,
 	 * pass hint.
 	 */
 	idxExprs = RelationGetIndexExpressions(indexRelation);
+	idxExprs = list_concat(idxExprs, RelationGetIndexExpressionsExpand(indexRelation));
 	hasexpression = index_expression_changed_walker((Node *) idxExprs,
 													allUpdatedCols);
 	list_free(idxExprs);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 40b09958..3aac6b51 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -828,7 +828,11 @@ make_ands_implicit(Expr *clause)
 
 /*
  * makeIndexInfo
- *	  create an IndexInfo node
+ *	  create an IndexInfo node.  Upon returning from this function,
+ * callers must apply ExpandVirtualGeneratedColumns()
+ * or RelationGetIndexExpressionsExpand or RelationGetIndexPredicateExpand() to
+ * ii_ExpressionsExpand and ii_PredicateExpand as needed for actual
+ * expansion when they are not NIL or dummy.
  */
 IndexInfo *
 makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
@@ -856,11 +860,15 @@ makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
 
 	/* expressions */
 	n->ii_Expressions = expressions;
+	n->ii_ExpressionsExpand = copyObject(expressions);
 	n->ii_ExpressionsState = NIL;
+	n->ii_ExpressionsExpandState = NIL;
 
 	/* predicates  */
 	n->ii_Predicate = predicates;
+	n->ii_PredicateExpand = copyObject(predicates);
 	n->ii_PredicateState = NULL;
+	n->ii_PredicateExpandState = NULL;
 
 	/* exclusion constraints */
 	n->ii_ExclusionOps = NULL;
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1c575e56..c51a7935 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -595,6 +595,12 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 	else
 	{
 		path->path.rows = baserel->rows;
+		/*
+		 * If index->predOK holds true, this index path's estimated
+		 * output tuples can exceed the index's total tuples.
+		*/
+		if (index->predOK && path->path.rows > index->tuples)
+			path->path.rows = index->tuples;
 		/* qpquals come from just the rel's restriction clauses */
 		qpquals = extract_nonindex_conditions(path->indexinfo->indrestrictinfo,
 											  path->indexclauses);
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 3f5d4fa3..23117e4a 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -267,7 +267,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
 		 * (generate_bitmap_or_paths() might be able to do something with
 		 * them, but that's of no concern here.)
 		 */
-		if (index->indpred != NIL && !index->predOK)
+		if (index->indpredExpand != NIL && !index->predOK)
 			continue;
 
 		/*
@@ -1119,7 +1119,7 @@ build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
 		 * just scanning the predOK index alone, no OR.)
 		 */
 		useful_predicate = false;
-		if (index->indpred != NIL)
+		if (index->indpredExpand != NIL)
 		{
 			if (index->predOK)
 			{
@@ -1131,10 +1131,10 @@ build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
 				if (all_clauses == NIL)
 					all_clauses = list_concat_copy(clauses, other_clauses);
 
-				if (!predicate_implied_by(index->indpred, all_clauses, false))
+				if (!predicate_implied_by(index->indpredExpand, all_clauses, false))
 					continue;	/* can't use it at all */
 
-				if (!predicate_implied_by(index->indpred, other_clauses, false))
+				if (!predicate_implied_by(index->indpredExpand, other_clauses, false))
 					useful_predicate = true;
 			}
 		}
@@ -2183,7 +2183,7 @@ find_indexpath_quals(Path *bitmapqual, List **quals, List **preds)
 
 			*quals = lappend(*quals, iclause->rinfo->clause);
 		}
-		*preds = list_concat(*preds, ipath->indexinfo->indpred);
+		*preds = list_concat(*preds, ipath->indexinfo->indpredExpand);
 	}
 	else
 		elog(ERROR, "unrecognized node type: %d", nodeTag(bitmapqual));
@@ -3959,7 +3959,7 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel)
 		IndexOptInfo *index = (IndexOptInfo *) lfirst(lc);
 
 		index->indrestrictinfo = rel->baserestrictinfo;
-		if (index->indpred)
+		if (index->indpredExpand)
 			have_partial = true;
 	}
 	if (!have_partial)
@@ -4038,11 +4038,11 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel)
 		IndexOptInfo *index = (IndexOptInfo *) lfirst(lc);
 		ListCell   *lcr;
 
-		if (index->indpred == NIL)
+		if (index->indpredExpand == NIL)
 			continue;			/* ignore non-partial indexes here */
 
 		if (!index->predOK)		/* don't repeat work if already proven OK */
-			index->predOK = predicate_implied_by(index->indpred, clauselist,
+			index->predOK = predicate_implied_by(index->indpredExpand, clauselist,
 												 false);
 
 		/* If rel is an update target, leave indrestrictinfo as set above */
@@ -4068,7 +4068,7 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel)
 			/* predicate_implied_by() assumes first arg is immutable */
 			if (contain_mutable_functions((Node *) rinfo->clause) ||
 				!predicate_implied_by(list_make1(rinfo->clause),
-									  index->indpred, false))
+									  index->indpredExpand, false))
 				index->indrestrictinfo = lappend(index->indrestrictinfo, rinfo);
 		}
 	}
@@ -4402,14 +4402,14 @@ match_index_to_operand(Node *operand,
 		int			i;
 		Node	   *indexkey;
 
-		indexpr_item = list_head(index->indexprs);
+		indexpr_item = list_head(index->indexprsExpand);
 		for (i = 0; i < indexcol; i++)
 		{
 			if (index->indexkeys[i] == 0)
 			{
 				if (indexpr_item == NULL)
 					elog(ERROR, "wrong number of index expressions");
-				indexpr_item = lnext(index->indexprs, indexpr_item);
+				indexpr_item = lnext(index->indexprsExpand, indexpr_item);
 			}
 		}
 		if (indexpr_item == NULL)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index de6a183d..2bdad7bd 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -3340,7 +3340,7 @@ create_bitmap_subplan(PlannerInfo *root, Path *bitmapqual,
 				subindexECs = lappend(subindexECs, rinfo->parent_ec);
 		}
 		/* We can add any index predicate conditions, too */
-		foreach(l, ipath->indexinfo->indpred)
+		foreach(l, ipath->indexinfo->indpredExpand)
 		{
 			Expr	   *pred = (Expr *) lfirst(l);
 
@@ -5142,7 +5142,7 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
 	}
 
 	/* It's an index expression, so find and cross-check the expression */
-	indexpr_item = list_head(index->indexprs);
+	indexpr_item = list_head(index->indexprsExpand);
 	for (pos = 0; pos < index->ncolumns; pos++)
 	{
 		if (index->indexkeys[pos] == 0)
@@ -5167,7 +5167,7 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
 				else
 					elog(ERROR, "index key does not match expected index column");
 			}
-			indexpr_item = lnext(index->indexprs, indexpr_item);
+			indexpr_item = lnext(index->indexprsExpand, indexpr_item);
 		}
 	}
 
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 7c4be174..72846d9e 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -433,24 +433,38 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 			 * properly reduced.
 			 */
 			info->indexprs = RelationGetIndexExpressions(indexRelation);
+			info->indexprsExpand = RelationGetIndexExpressionsExpand(indexRelation);
 			info->indpred = RelationGetIndexPredicate(indexRelation);
+			info->indpredExpand = RelationGetIndexPredicateExpand(indexRelation);
 			if (info->indexprs)
 			{
 				if (varno != 1)
+				{
 					ChangeVarNodes((Node *) info->indexprs, 1, varno, 0);
+					ChangeVarNodes((Node *) info->indexprsExpand, 1, varno, 0);
+				}
 
 				info->indexprs = (List *)
 					eval_const_expressions(root, (Node *) info->indexprs);
+				info->indexprsExpand = (List *)
+					eval_const_expressions(root, (Node *) info->indexprsExpand);
 			}
 			if (info->indpred)
 			{
 				if (varno != 1)
+				{
 					ChangeVarNodes((Node *) info->indpred, 1, varno, 0);
+					ChangeVarNodes((Node *) info->indpredExpand, 1, varno, 0);
+				}
 
 				info->indpred = (List *)
 					eval_const_expressions(root,
 										   (Node *) make_ands_explicit(info->indpred));
+				info->indpredExpand = (List *)
+					eval_const_expressions(root,
+										   (Node *) make_ands_explicit(info->indpredExpand));
 				info->indpred = make_ands_implicit((Expr *) info->indpred);
+				info->indpredExpand = make_ands_implicit((Expr *) info->indpredExpand);
 			}
 
 			/* Build targetlist using the completed indexprs data */
@@ -941,8 +955,8 @@ infer_arbiter_indexes(PlannerInfo *root)
 										   attno - FirstLowInvalidHeapAttributeNumber);
 				}
 
-				inferElems = RelationGetIndexExpressions(idxRel);
-				inferIndexExprs = RelationGetIndexPredicate(idxRel);
+				inferElems = RelationGetIndexExpressionsExpand(idxRel);
+				inferIndexExprs = RelationGetIndexPredicateExpand(idxRel);
 				break;
 			}
 		}
@@ -1072,7 +1086,7 @@ infer_arbiter_indexes(PlannerInfo *root)
 			continue;
 
 		/* Expression attributes (if any) must match */
-		idxExprs = RelationGetIndexExpressions(idxRel);
+		idxExprs = RelationGetIndexExpressionsExpand(idxRel);
 		if (idxExprs)
 		{
 			if (varno != 1)
@@ -1139,7 +1153,7 @@ infer_arbiter_indexes(PlannerInfo *root)
 		if (list_difference(idxExprs, inferElems) != NIL)
 			continue;
 
-		predExprs = RelationGetIndexPredicate(idxRel);
+		predExprs = RelationGetIndexPredicateExpand(idxRel);
 		if (predExprs)
 		{
 			if (varno != 1)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d6efd070..bea5ef1b 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -7431,6 +7431,7 @@ genericcostestimate(PlannerInfo *root,
 	double		qual_arg_cost;
 	List	   *selectivityQuals;
 	ListCell   *l;
+	bool		needCalcIndexSelectivity = false;
 
 	/*
 	 * If the index is partial, AND the index predicate with the explicitly
@@ -7495,9 +7496,20 @@ genericcostestimate(PlannerInfo *root,
 	 * indexSelectivity estimate is tiny.
 	 */
 	if (numIndexTuples > index->tuples)
+	{
 		numIndexTuples = index->tuples;
+		needCalcIndexSelectivity = true;
+	}
+	/* Recalculate indexSelectivity based on the estimated numIndexTuples */
 	if (numIndexTuples < 1.0)
+	{
 		numIndexTuples = 1.0;
+		indexSelectivity = 0.0;
+	}
+	else if (needCalcIndexSelectivity && index->rel->tuples > 0)
+	{
+		indexSelectivity = numIndexTuples / index->rel->tuples;
+	}
 
 	/*
 	 * Estimate the number of index pages that will be retrieved.
@@ -7639,10 +7651,10 @@ add_predicate_to_index_quals(IndexOptInfo *index, List *indexQuals)
 	List	   *predExtraQuals = NIL;
 	ListCell   *lc;
 
-	if (index->indpred == NIL)
+	if (index->indpredExpand == NIL)
 		return indexQuals;
 
-	foreach(lc, index->indpred)
+	foreach(lc, index->indpredExpand)
 	{
 		Node	   *predQual = (Node *) lfirst(lc);
 		List	   *oneQual = list_make1(predQual);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0572ab42..056eb193 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -91,6 +91,7 @@
 #include "utils/resowner.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "rewrite/rewriteHandler.h"
 
 #define RELCACHE_INIT_FILEMAGIC		0x573266	/* version ID value */
 
@@ -1583,7 +1584,9 @@ RelationInitIndexAccessInfo(Relation relation)
 	 * expressions, predicate, exclusion caches will be filled later
 	 */
 	relation->rd_indexprs = NIL;
+	relation->rd_indexprsExpand = NIL;
 	relation->rd_indpred = NIL;
+	relation->rd_indpredExpand = NIL;
 	relation->rd_exclops = NULL;
 	relation->rd_exclprocs = NULL;
 	relation->rd_exclstrats = NULL;
@@ -5146,6 +5149,45 @@ RelationGetIndexExpressions(Relation relation)
 	return result;
 }
 
+List *
+RelationGetIndexExpressionsExpand(Relation relation)
+{
+	List	   *result;
+	MemoryContext oldcxt;
+	bool		isnull;
+	Datum		heapRelidDatum;
+	Oid         heapRelid;
+
+	/* Quick exit if we already computed the result. */
+	if (relation->rd_indexprsExpand)
+		return copyObject(relation->rd_indexprsExpand);
+
+	/* Quick exit if there is nothing to do. */
+	if (relation->rd_indextuple == NULL ||
+		heap_attisnull(relation->rd_indextuple, Anum_pg_index_indexprs, NULL))
+		return NIL;
+
+	result = RelationGetIndexExpressions(relation);
+	if (result == NIL)
+		return NIL;
+
+	heapRelidDatum = heap_getattr(relation->rd_indextuple,
+							 Anum_pg_index_indrelid,
+							 GetPgIndexDescriptor(),
+							 &isnull);
+	Assert(!isnull);
+	heapRelid = DatumGetObjectId(heapRelidDatum);
+
+	result = ExpandVirtualGeneratedColumns(result, NULL, heapRelid);
+
+	/* Now save a copy of the completed tree in the relcache entry. */
+	oldcxt = MemoryContextSwitchTo(relation->rd_indexcxt);
+	relation->rd_indexprsExpand = copyObject(result);
+	MemoryContextSwitchTo(oldcxt);
+
+	return result;
+}
+
 /*
  * RelationGetDummyIndexExpressions -- get dummy expressions for an index
  *
@@ -5266,6 +5308,45 @@ RelationGetIndexPredicate(Relation relation)
 	return result;
 }
 
+List *
+RelationGetIndexPredicateExpand(Relation relation)
+{
+	List	   *result;
+	MemoryContext oldcxt;
+	bool		isnull;
+	Datum		heapRelidDatum;
+	Oid         heapRelid;
+
+	/* Quick exit if we already computed the result. */
+	if (relation->rd_indpredExpand)
+		return copyObject(relation->rd_indpredExpand);
+
+	/* Quick exit if there is nothing to do. */
+	if (relation->rd_indextuple == NULL ||
+		heap_attisnull(relation->rd_indextuple, Anum_pg_index_indpred, NULL))
+		return NIL;
+
+	result = RelationGetIndexPredicate(relation);
+	if (result == NIL)
+		return NIL;
+
+	heapRelidDatum = heap_getattr(relation->rd_indextuple,
+							 Anum_pg_index_indrelid,
+							 GetPgIndexDescriptor(),
+							 &isnull);
+	Assert(!isnull);
+	heapRelid = DatumGetObjectId(heapRelidDatum);
+
+	result = ExpandVirtualGeneratedColumns(result, NULL, heapRelid);
+
+	/* Now save a copy of the completed tree in the relcache entry. */
+	oldcxt = MemoryContextSwitchTo(relation->rd_indexcxt);
+	relation->rd_indpredExpand = copyObject(result);
+	MemoryContextSwitchTo(oldcxt);
+
+	return result;
+}
+
 /*
  * RelationGetIndexAttrBitmap -- get a bitmap of index attribute numbers
  *
@@ -6490,7 +6571,9 @@ load_relcache_init_file(bool shared)
 		rel->rd_partcheckvalid = false;
 		rel->rd_partcheckcxt = NULL;
 		rel->rd_indexprs = NIL;
+		rel->rd_indexprsExpand = NIL;
 		rel->rd_indpred = NIL;
+		rel->rd_indpredExpand = NIL;
 		rel->rd_exclops = NULL;
 		rel->rd_exclprocs = NULL;
 		rel->rd_exclstrats = NULL;
@@ -7019,3 +7102,25 @@ ResOwnerReleaseRelation(Datum res)
 
 	RelationCloseCleanup((Relation) DatumGetPointer(res));
 }
+
+List *
+ExpandVirtualGeneratedColumns(List *list, Relation heapRelation, Oid heapRelId)
+{
+	bool	opened_relation = false;
+
+	if (list == NIL || (heapRelation == NULL && heapRelId == InvalidOid))
+		return list;
+
+	if (heapRelation == NULL)
+	{
+		heapRelation = table_open(heapRelId, NoLock);
+		opened_relation = true;
+	}
+
+	list = (List *)expand_generated_columns_in_expr((Node *)list, heapRelation, 1);
+
+	if (opened_relation)
+		table_close(heapRelation, NoLock);
+
+	return list;
+}
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 53c13831..20a2e618 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -190,13 +190,17 @@ typedef struct IndexInfo
 
 	/* expr trees for expression entries, or NIL if none */
 	List	   *ii_Expressions; /* list of Expr */
+	List	   *ii_ExpressionsExpand; 	/* list of Expr */
 	/* exec state for expressions, or NIL if none */
 	List	   *ii_ExpressionsState;	/* list of ExprState */
+	List	   *ii_ExpressionsExpandState;	/* list of ExprState */
 
 	/* partial-index predicate, or NIL if none */
 	List	   *ii_Predicate;	/* list of Expr */
+	List	   *ii_PredicateExpand;		/* list of Expr */
 	/* exec state for expressions, or NIL if none */
 	ExprState  *ii_PredicateState;
+	ExprState  *ii_PredicateExpandState;
 
 	/* Per-column exclusion operators, or NULL if none */
 	Oid		   *ii_ExclusionOps;	/* array with one entry per column */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c681..9c65e123 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1403,8 +1403,10 @@ typedef struct IndexOptInfo
 	 * print indextlist
 	 */
 	List	   *indexprs pg_node_attr(read_write_ignore);
+	List	   *indexprsExpand pg_node_attr(read_write_ignore);
 	/* predicate if a partial index, else NIL */
 	List	   *indpred;
+	List	   *indpredExpand;
 
 	/* targetlist representing index columns */
 	List	   *indextlist;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8..7bd01b36 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -210,7 +210,9 @@ typedef struct RelationData
 	struct FmgrInfo *rd_supportinfo;	/* lookup info for support procedures */
 	int16	   *rd_indoption;	/* per-column AM-specific flags */
 	List	   *rd_indexprs;	/* index expression trees, if any */
+	List	   *rd_indexprsExpand;	/* expanded index expression trees, if any */
 	List	   *rd_indpred;		/* index predicate tree, if any */
+	List	   *rd_indpredExpand;		/* expanded index predicate tree, if any */
 	Oid		   *rd_exclops;		/* OIDs of exclusion operators, if any */
 	Oid		   *rd_exclprocs;	/* OIDs of exclusion ops' procs, if any */
 	uint16	   *rd_exclstrats;	/* exclusion ops' strategy numbers, if any */
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 89c27aa1..70531bb8 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -58,8 +58,10 @@ extern List *RelationGetStatExtList(Relation relation);
 extern Oid	RelationGetPrimaryKeyIndex(Relation relation, bool deferrable_ok);
 extern Oid	RelationGetReplicaIndex(Relation relation);
 extern List *RelationGetIndexExpressions(Relation relation);
+extern List *RelationGetIndexExpressionsExpand(Relation relation);
 extern List *RelationGetDummyIndexExpressions(Relation relation);
 extern List *RelationGetIndexPredicate(Relation relation);
+extern List *RelationGetIndexPredicateExpand(Relation relation);
 extern bytea **RelationGetIndexAttOptions(Relation relation, bool copy);
 
 /*
@@ -161,4 +163,6 @@ extern PGDLLIMPORT bool criticalRelcachesBuilt;
 /* should be used only by relcache.c and postinit.c */
 extern PGDLLIMPORT bool criticalSharedRelcachesBuilt;
 
+extern List *ExpandVirtualGeneratedColumns(List *list, Relation heapRelation, Oid heapRelId);
+
 #endif							/* RELCACHE_H */
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 24d5dbf4..8831add0 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -763,39 +763,202 @@ ERROR:  column "c" of relation "gtestnn_child" contains null values
 ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) VIRTUAL;  -- ok
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
-ERROR:  unique constraints on virtual generated columns are not supported
---INSERT INTO gtest22a VALUES (2);
---INSERT INTO gtest22a VALUES (3);
---INSERT INTO gtest22a VALUES (4);
+INSERT INTO gtest22a VALUES (2);  -- ok
+INSERT INTO gtest22a VALUES (3);  -- error
+ERROR:  duplicate key value violates unique constraint "gtest22a_b_key"
+DETAIL:  Key (b)=(1) already exists.
+INSERT INTO gtest22a VALUES (4);  -- ok
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
-ERROR:  primary keys on virtual generated columns are not supported
---INSERT INTO gtest22b VALUES (2);
---INSERT INTO gtest22b VALUES (2);
+INSERT INTO gtest22b VALUES (2);  -- ok
+INSERT INTO gtest22b VALUES (2);  -- error
+ERROR:  duplicate key value violates unique constraint "gtest22b_pkey"
+DETAIL:  Key (a, b)=(2, 1) already exists.
 -- indexes
 CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
---CREATE INDEX gtest22c_b_idx ON gtest22c (b);
---CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));
---CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;
---\d gtest22c
---INSERT INTO gtest22c VALUES (1), (2), (3);
---SET enable_seqscan TO off;
---SET enable_bitmapscan TO off;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4;
---SELECT * FROM gtest22c WHERE b = 4;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6;
---SELECT * FROM gtest22c WHERE b * 3 = 6;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
---ANALYZE gtest22c;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
---SELECT * FROM gtest22c WHERE b = 8;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
---SELECT * FROM gtest22c WHERE b * 3 = 12;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---RESET enable_seqscan;
---RESET enable_bitmapscan;
+CREATE INDEX gtest22c_b_idx ON gtest22c (b);  -- ok
+CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));  -- ok
+CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;  -- ok
+\d gtest22c
+               Table "generated_virtual_tests.gtest22c"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
+ a      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2)
+Indexes:
+    "gtest22c_b_idx" btree (b)
+    "gtest22c_expr_idx" btree ((b * 3))
+    "gtest22c_pred_idx" btree (a) WHERE b > 0
+
+INSERT INTO gtest22c VALUES (1), (2), (3);
+SET enable_seqscan TO off;
+SET enable_bitmapscan TO off;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Index Only Scan using gtest22c_pred_idx on gtest22c
+   Filter: ((a * 2) = 4)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE b = 4;
+ a | b 
+---+---
+ 2 | 4
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6;
+                   QUERY PLAN                   
+------------------------------------------------
+ Index Scan using gtest22c_expr_idx on gtest22c
+   Index Cond: (((a * 2) * 3) = 6)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE b * 3 = 6;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Index Only Scan using gtest22c_pred_idx on gtest22c
+   Index Cond: (a = 1)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
+ANALYZE gtest22c;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Index Only Scan using gtest22c_pred_idx on gtest22c
+   Filter: ((a * 4) = 8)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE b = 8;
+ a | b 
+---+---
+ 2 | 8
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
+                   QUERY PLAN                   
+------------------------------------------------
+ Index Scan using gtest22c_expr_idx on gtest22c
+   Index Cond: (((a * 4) * 3) = 12)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE b * 3 = 12;
+ a | b 
+---+---
+ 1 | 4
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Index Only Scan using gtest22c_pred_idx on gtest22c
+   Index Cond: (a = 1)
+(2 rows)
+
+SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+ a | b 
+---+---
+ 1 | 4
+(1 row)
+
+CREATE TABLE gtest22d (a int
+  ,v1 int GENERATED ALWAYS AS (a * 11) VIRTUAL
+  ,v2 int GENERATED ALWAYS AS (a * 22) VIRTUAL
+  ,v3 int GENERATED ALWAYS AS (a * 33) VIRTUAL
+  ,v4 int GENERATED ALWAYS AS (a * 44) VIRTUAL
+);  -- ok
+insert into gtest22d select oid from pg_catalog.pg_class union all select -1;  -- ok
+CREATE INDEX gtest22d_vgc_idx ON gtest22d(a,v1,abs(v2)) Include(v3) WHERE v4 < 100;  -- ok
+\d gtest22d
+                Table "generated_virtual_tests.gtest22d"
+ Column |  Type   | Collation | Nullable |           Default            
+--------+---------+-----------+----------+------------------------------
+ a      | integer |           |          | 
+ v1     | integer |           |          | generated always as (a * 11)
+ v2     | integer |           |          | generated always as (a * 22)
+ v3     | integer |           |          | generated always as (a * 33)
+ v4     | integer |           |          | generated always as (a * 44)
+Indexes:
+    "gtest22d_vgc_idx" btree (a, v1, abs(v2)) INCLUDE (v3) WHERE v4 < 100
+
+analyze gtest22d;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE v4 < 100;  --Index Only Scan or Index Scan
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using gtest22d_vgc_idx on gtest22d
+(1 row)
+
+SELECT * FROM gtest22d WHERE v4 < 100;
+ a  | v1  | v2  | v3  | v4  
+----+-----+-----+-----+-----
+ -1 | -11 | -22 | -33 | -44
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE v4 * 5 < 100;  --Seq Scan
+            QUERY PLAN            
+----------------------------------
+ Seq Scan on gtest22d
+   Disabled: true
+   Filter: (((a * 44) * 5) < 100)
+(3 rows)
+
+SELECT * FROM gtest22d WHERE v4 * 5 < 100;
+ a  | v1  | v2  | v3  | v4  
+----+-----+-----+-----+-----
+ -1 | -11 | -22 | -33 | -44
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE a * 44 < 100;  --Index Only Scan or Index Scan
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using gtest22d_vgc_idx on gtest22d
+(1 row)
+
+SELECT * FROM gtest22d WHERE a * 44 < 100;
+ a  | v1  | v2  | v3  | v4  
+----+-----+-----+-----+-----
+ -1 | -11 | -22 | -33 | -44
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE (a * 44) < 100;  --Index Only Scan or Index Scan
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using gtest22d_vgc_idx on gtest22d
+(1 row)
+
+SELECT * FROM gtest22d WHERE (a * 44) < 100;
+ a  | v1  | v2  | v3  | v4  
+----+-----+-----+-----+-----
+ -1 | -11 | -22 | -33 | -44
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE (a * 33) < 100;  --Seq Scan
+         QUERY PLAN         
+----------------------------
+ Seq Scan on gtest22d
+   Disabled: true
+   Filter: ((a * 33) < 100)
+(3 rows)
+
+SELECT * FROM gtest22d WHERE (a * 33) < 100;
+ a  | v1  | v2  | v3  | v4  
+----+-----+-----+-----+-----
+ -1 | -11 | -22 | -33 | -44
+(1 row)
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
 -- foreign keys
 CREATE TABLE gtest23a (x int PRIMARY KEY, y int);
 --INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33);
@@ -813,12 +976,12 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
 --DROP TABLE gtest23b;
 --DROP TABLE gtest23a;
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
-ERROR:  primary keys on virtual generated columns are not supported
---INSERT INTO gtest23p VALUES (1), (2), (3);
+INSERT INTO gtest23p VALUES (1), (2), (3);  -- ok
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
-ERROR:  relation "gtest23p" does not exist
---INSERT INTO gtest23q VALUES (1, 2);  -- ok
---INSERT INTO gtest23q VALUES (2, 5);  -- error
+INSERT INTO gtest23q VALUES (1, 2);  -- ok
+INSERT INTO gtest23q VALUES (2, 5);  -- error
+ERROR:  insert or update on table "gtest23q" violates foreign key constraint "gtest23q_b_fkey"
+DETAIL:  Key (b)=(5) is not present in table "gtest23p".
 -- domains
 CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
 CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index 929feda6..abe4169f 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -1785,3 +1785,36 @@ insert into test_pg_wholerow_index values (2, 'addition', 0);
 drop index row_image_index;
 drop function row_image(test_pg_wholerow_index);
 drop table test_pg_wholerow_index;
+-- test creation of a predicate index with a whole-row expression
+DROP TABLE IF EXISTS test_pg_wholerow;
+NOTICE:  table "test_pg_wholerow" does not exist, skipping
+CREATE TABLE test_pg_wholerow(a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL, c text);
+CREATE UNIQUE INDEX test_pg_wholerow_pred_idx ON test_pg_wholerow(a) WHERE test_pg_wholerow IS NOT NULL;
+INSERT INTO test_pg_wholerow(a,c) SELECT NULL, NULL FROM generate_series(1, 1000);
+INSERT INTO test_pg_wholerow(a,c) SELECT 1, '1';
+ANALYZE test_pg_wholerow;
+SELECT relname,reltuples FROM pg_catalog.pg_class WHERE relname IN ('test_pg_wholerow', 'test_pg_wholerow_pred_idx') ORDER BY OID;
+          relname          | reltuples 
+---------------------------+-----------
+ test_pg_wholerow          |      1001
+ test_pg_wholerow_pred_idx |         1
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT *   FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Index Scan using test_pg_wholerow_pred_idx on test_pg_wholerow
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT a   FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Index Only Scan using test_pg_wholerow_pred_idx on test_pg_wholerow
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT a,b FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Index Only Scan using test_pg_wholerow_pred_idx on test_pg_wholerow
+(1 row)
+
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index 9c2bb659..5ebdc5ef 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -396,40 +396,62 @@ ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif
 
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
---INSERT INTO gtest22a VALUES (2);
---INSERT INTO gtest22a VALUES (3);
---INSERT INTO gtest22a VALUES (4);
+INSERT INTO gtest22a VALUES (2);  -- ok
+INSERT INTO gtest22a VALUES (3);  -- error
+INSERT INTO gtest22a VALUES (4);  -- ok
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
---INSERT INTO gtest22b VALUES (2);
---INSERT INTO gtest22b VALUES (2);
+INSERT INTO gtest22b VALUES (2);  -- ok
+INSERT INTO gtest22b VALUES (2);  -- error
 
 -- indexes
 CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
---CREATE INDEX gtest22c_b_idx ON gtest22c (b);
---CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));
---CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;
---\d gtest22c
-
---INSERT INTO gtest22c VALUES (1), (2), (3);
---SET enable_seqscan TO off;
---SET enable_bitmapscan TO off;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4;
---SELECT * FROM gtest22c WHERE b = 4;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6;
---SELECT * FROM gtest22c WHERE b * 3 = 6;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
-
---ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
---ANALYZE gtest22c;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
---SELECT * FROM gtest22c WHERE b = 8;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
---SELECT * FROM gtest22c WHERE b * 3 = 12;
---EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
---RESET enable_seqscan;
---RESET enable_bitmapscan;
+CREATE INDEX gtest22c_b_idx ON gtest22c (b);  -- ok
+CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));  -- ok
+CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;  -- ok
+\d gtest22c
+
+INSERT INTO gtest22c VALUES (1), (2), (3);
+SET enable_seqscan TO off;
+SET enable_bitmapscan TO off;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4;
+SELECT * FROM gtest22c WHERE b = 4;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6;
+SELECT * FROM gtest22c WHERE b * 3 = 6;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+
+ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
+ANALYZE gtest22c;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
+SELECT * FROM gtest22c WHERE b = 8;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
+SELECT * FROM gtest22c WHERE b * 3 = 12;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+
+CREATE TABLE gtest22d (a int
+  ,v1 int GENERATED ALWAYS AS (a * 11) VIRTUAL
+  ,v2 int GENERATED ALWAYS AS (a * 22) VIRTUAL
+  ,v3 int GENERATED ALWAYS AS (a * 33) VIRTUAL
+  ,v4 int GENERATED ALWAYS AS (a * 44) VIRTUAL
+);  -- ok
+insert into gtest22d select oid from pg_catalog.pg_class union all select -1;  -- ok
+CREATE INDEX gtest22d_vgc_idx ON gtest22d(a,v1,abs(v2)) Include(v3) WHERE v4 < 100;  -- ok
+\d gtest22d
+analyze gtest22d;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE v4 < 100;  --Index Only Scan or Index Scan
+SELECT * FROM gtest22d WHERE v4 < 100;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE v4 * 5 < 100;  --Seq Scan
+SELECT * FROM gtest22d WHERE v4 * 5 < 100;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE a * 44 < 100;  --Index Only Scan or Index Scan
+SELECT * FROM gtest22d WHERE a * 44 < 100;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE (a * 44) < 100;  --Index Only Scan or Index Scan
+SELECT * FROM gtest22d WHERE (a * 44) < 100;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22d WHERE (a * 33) < 100;  --Seq Scan
+SELECT * FROM gtest22d WHERE (a * 33) < 100;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
 
 -- foreign keys
 CREATE TABLE gtest23a (x int PRIMARY KEY, y int);
@@ -450,11 +472,11 @@ CREATE TABLE gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRT
 --DROP TABLE gtest23a;
 
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
---INSERT INTO gtest23p VALUES (1), (2), (3);
+INSERT INTO gtest23p VALUES (1), (2), (3);  -- ok
 
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
---INSERT INTO gtest23q VALUES (1, 2);  -- ok
---INSERT INTO gtest23q VALUES (2, 5);  -- error
+INSERT INTO gtest23q VALUES (1, 2);  -- ok
+INSERT INTO gtest23q VALUES (2, 5);  -- error
 
 -- domains
 CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 3d43af33..1152bcee 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -1005,3 +1005,15 @@ insert into test_pg_wholerow_index values (2, 'addition', 0);
 drop index row_image_index;
 drop function row_image(test_pg_wholerow_index);
 drop table test_pg_wholerow_index;
+
+-- test creation of a predicate index with a whole-row expression
+DROP TABLE IF EXISTS test_pg_wholerow;
+CREATE TABLE test_pg_wholerow(a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL, c text);
+CREATE UNIQUE INDEX test_pg_wholerow_pred_idx ON test_pg_wholerow(a) WHERE test_pg_wholerow IS NOT NULL;
+INSERT INTO test_pg_wholerow(a,c) SELECT NULL, NULL FROM generate_series(1, 1000);
+INSERT INTO test_pg_wholerow(a,c) SELECT 1, '1';
+ANALYZE test_pg_wholerow;
+SELECT relname,reltuples FROM pg_catalog.pg_class WHERE relname IN ('test_pg_wholerow', 'test_pg_wholerow_pred_idx') ORDER BY OID;
+EXPLAIN (COSTS OFF) SELECT *   FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
+EXPLAIN (COSTS OFF) SELECT a   FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
+EXPLAIN (COSTS OFF) SELECT a,b FROM test_pg_wholerow WHERE test_pg_wholerow IS NOT NULL;
\ No newline at end of file
-- 
2.43.0

