diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index 487373b49703fe3c6729f1c39d985fdbb590d351..e3742cf71d05c88e8e7a557144261eacc5837654 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -26,8 +26,6 @@
 
 #include "access/relscan.h"
 #include "access/visibilitymap.h"
-#include "catalog/pg_opfamily.h"
-#include "catalog/pg_type.h"
 #include "executor/execdebug.h"
 #include "executor/nodeIndexonlyscan.h"
 #include "executor/nodeIndexscan.h"
@@ -162,8 +160,10 @@ StoreIndexTuple(TupleTableSlot *slot, IndexTuple itup, Relation indexRel)
 	int			i;
 
 	/*
-	 * Note: we must use the index relation's tupdesc in index_getattr,
-	 * not the slot's tupdesc, because of index_descriptor_hack().
+	 * Note: we must use the index relation's tupdesc in index_getattr, not
+	 * the slot's tupdesc, in case the latter has different datatypes (this
+	 * happens for btree name_ops in particular).  They'd better have the same
+	 * number of columns though.
 	 */
 	Assert(slot->tts_tupleDescriptor->natts == nindexatts);
 
@@ -173,45 +173,6 @@ StoreIndexTuple(TupleTableSlot *slot, IndexTuple itup, Relation indexRel)
 	ExecStoreVirtualTuple(slot);
 }
 
-/*
- * index_descriptor_hack -- ugly kluge to make index's tupdesc OK for slot
- *
- * This is necessary because, alone among btree opclasses, name_ops uses
- * a storage type (cstring) different from its input type.  The index
- * tuple descriptor will show "cstring", which is correct, but we have to
- * expose "name" as the slot datatype or ExecEvalVar will whine.  If we
- * ever want to have any other cases with a different storage type, we ought
- * to think of a cleaner solution than this.
- */
-static TupleDesc
-index_descriptor_hack(Relation indexRel)
-{
-	TupleDesc	tupdesc = RelationGetDescr(indexRel);
-	int			i;
-
-	/* copy so we can scribble on it safely */
-	tupdesc = CreateTupleDescCopy(tupdesc);
-
-	for (i = 0; i < tupdesc->natts; i++)
-	{
-		if (indexRel->rd_opfamily[i] == NAME_BTREE_FAM_OID &&
-			tupdesc->attrs[i]->atttypid == CSTRINGOID)
-		{
-			tupdesc->attrs[i]->atttypid = NAMEOID;
-
-			/*
-			 * We set attlen to match the type OID just in case anything looks
-			 * at it.  Note that this is safe only because StoreIndexTuple
-			 * will insert the data as a virtual tuple, and we don't expect
-			 * anything will try to materialize the scan tuple slot.
-			 */
-			tupdesc->attrs[i]->attlen = NAMEDATALEN;
-		}
-	}
-
-	return tupdesc;
-}
-
 /*
  * IndexOnlyRecheck -- access method routine to recheck a tuple in EvalPlanQual
  *
@@ -426,9 +387,20 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 	indexstate->ss.ss_currentScanDesc = NULL;	/* no heap scan here */
 
 	/*
-	 * Initialize result tuple type.
+	 * Build the scan tuple type using the indextlist generated by the
+	 * planner.  We use this, rather than the index's physical tuple
+	 * descriptor, because the latter contains storage column types not the
+	 * types of the original datums.  (It's the AM's responsibility to return
+	 * suitable data anyway.)
+	 */
+	tupDesc = ExecTypeFromTL(node->indextlist, false);
+	ExecAssignScanType(&indexstate->ss, tupDesc);
+
+	/*
+	 * Initialize result tuple type and projection info.
 	 */
 	ExecAssignResultTypeFromTL(&indexstate->ss.ps);
+	ExecAssignScanProjectionInfo(&indexstate->ss);
 
 	/*
 	 * If we are just doing EXPLAIN (ie, aren't going to run the plan), stop
@@ -449,14 +421,6 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 	indexstate->ioss_RelationDesc = index_open(node->indexid,
 									 relistarget ? NoLock : AccessShareLock);
 
-	/*
-	 * Now we can get the scan tuple's type (which is the index's rowtype,
-	 * not the heap's) and initialize result projection info.
-	 */
-	tupDesc = index_descriptor_hack(indexstate->ioss_RelationDesc);
-	ExecAssignScanType(&indexstate->ss, tupDesc);
-	ExecAssignScanProjectionInfo(&indexstate->ss);
-
 	/*
 	 * Initialize index-specific scan state
 	 */