diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3f02202cafb6adf18e7cb083f5c2052e38233cc4..71e20f27409041a855eeb13948b4afabd523b8af 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2995,6 +2995,29 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><symbol>DEPENDENCY_INTERNAL_AUTO</symbol> (<literal>I</literal>)</term>
+     <listitem>
+      <para>
+       The dependent object was created as part of creation of the
+       referenced object, and is really just a part of its internal
+       implementation.  A <command>DROP</command> of the dependent object
+       will be disallowed outright (we'll tell the user to issue a
+       <command>DROP</command> against the referenced object, instead).
+       While a regular internal dependency will prevent
+       the dependent object from being dropped while any such dependencies
+       remain, <literal>DEPENDENCY_INTERNAL_AUTO</literal> will allow such
+       a drop as long as the object can be found by following any of such
+       dependencies.
+       Example: an index on a partition is made internal-auto-dependent on
+       both the partition itself as well as on the index on the parent
+       partitioned table; so the partition index is dropped together with
+       either the partition it indexes, or with the parent index it is
+       attached to.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><symbol>DEPENDENCY_EXTENSION</symbol> (<literal>e</literal>)</term>
      <listitem>
diff --git a/doc/src/sgml/ref/alter_index.sgml b/doc/src/sgml/ref/alter_index.sgml
index e54237272c61a6fdd428ce1419dddce0fda099e3..c0606689f062c2f4b8ea7764a425a99d886a4db2 100644
--- a/doc/src/sgml/ref/alter_index.sgml
+++ b/doc/src/sgml/ref/alter_index.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
 <synopsis>
 ALTER INDEX [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
 ALTER INDEX [ IF EXISTS ] <replaceable class="parameter">name</replaceable> SET TABLESPACE <replaceable class="parameter">tablespace_name</replaceable>
+ALTER INDEX <replaceable class="parameter">name</replaceable> ATTACH PARTITION <replaceable class="parameter">index_name</replaceable>
 ALTER INDEX <replaceable class="parameter">name</replaceable> DEPENDS ON EXTENSION <replaceable class="parameter">extension_name</replaceable>
 ALTER INDEX [ IF EXISTS ] <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">storage_parameter</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] )
 ALTER INDEX [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET ( <replaceable class="parameter">storage_parameter</replaceable> [, ... ] )
@@ -75,6 +76,19 @@ ALTER INDEX ALL IN TABLESPACE <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ATTACH PARTITION</literal></term>
+    <listitem>
+     <para>
+      Causes the named index to become attached to the altered index.
+      The named index must be on a partition of the table containing the
+      index being altered, and have an equivalent definition.  An attached
+      index cannot be dropped by itself, and will automatically be dropped
+      if its parent index is dropped.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>DEPENDS ON EXTENSION</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 686bb2c11c5a44bde554c7f30f54c8eaadbbf0dd..286c7a858972e712dd6d15f6fcecf6e186959f9a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -805,7 +805,10 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
       as a partition of the target table. The table can be attached
       as a partition for specific values using <literal>FOR VALUES
       </literal> or as a default partition by using <literal>DEFAULT
-      </literal>.
+      </literal>.  For each index in the target table, a corresponding
+      one will be created in the attached table; or, if an equivalent
+      index already exists, will be attached to the target table's index,
+      as if <command>ALTER INDEX ATTACH PARTITION</command> had been executed.
      </para>
 
      <para>
@@ -866,7 +869,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
      <para>
       This form detaches specified partition of the target table.  The detached
       partition continues to exist as a standalone table, but no longer has any
-      ties to the table from which it was detached.
+      ties to the table from which it was detached.  Any indexes that were
+      attached to the target table's indexes are detached.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 025537575b880d8bbb1a17121948ee224096eb8b..5137fe63832f75195fb49bb3f7c76a1be2658345 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> ] ON <replaceable class="parameter">table_name</replaceable> [ USING <replaceable class="parameter">method</replaceable> ]
+CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> ] ON [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ USING <replaceable class="parameter">method</replaceable> ]
     ( { <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
@@ -151,6 +151,16 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><literal>ONLY</literal></term>
+      <listitem>
+       <para>
+        Indicates not to recurse creating indexes on partitions, if the
+        table is partitioned.  The default is to recurse.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><replaceable class="parameter">table_name</replaceable></term>
       <listitem>
@@ -545,6 +555,27 @@ Indexes:
    linkend="xindex"/>.
   </para>
 
+  <para>
+   When <literal>CREATE INDEX</literal> is invoked on a partitioned
+   table, the default behavior is to recurse to all partitions to ensure
+   they all have matching indexes.
+   Each partition is first checked to determine whether an equivalent
+   index already exists, and if so, that index will become attached as a
+   partition index to the index being created, which will become its
+   parent index.
+   If no matching index exists, a new index will be created and
+   automatically attached; the name of the new index in each partition
+   will be determined as if no index name had been specified in the
+   command.
+   If the <literal>ONLY</literal> option is specified, no recursion
+   is done, and the index is marked invalid
+   (<command>ALTER INDEX ... ATTACH PARTITION</command> turns the index
+   valid, once all partitions acquire the index.)  Note, however, that
+   any partition that is created in the future using
+   <command>CREATE TABLE ... PARTITION OF</command> will automatically
+   contain the index regardless of whether this option was specified.
+  </para>
+
   <para>
    For index methods that support ordered scans (currently, only B-tree),
    the optional clauses <literal>ASC</literal>, <literal>DESC</literal>, <literal>NULLS
diff --git a/doc/src/sgml/ref/reindex.sgml b/doc/src/sgml/ref/reindex.sgml
index 79f6931c6a2ac44ac6d6330d8fa41b7b6b406f29..1c21fafb80e6ac779adb08e25f0a7f91a21baba9 100644
--- a/doc/src/sgml/ref/reindex.sgml
+++ b/doc/src/sgml/ref/reindex.sgml
@@ -231,6 +231,11 @@ REINDEX [ ( VERBOSE ) ] { INDEX | TABLE | SCHEMA | DATABASE | SYSTEM } <replacea
    reindex anything.
   </para>
 
+  <para>
+   Reindexing partitioned tables or partitioned indexes is not supported.
+   Each individual partition can be reindexed separately instead.
+  </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 425bc5d06ea0664c74d8c6116692aae3c6780fb3..274f7aa8e9762cd773577824b4adb411ef4c0748 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -993,6 +993,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
 			options = view_reloptions(datum, false);
 			break;
 		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
 			options = index_reloptions(amoptions, datum, false);
 			break;
 		case RELKIND_FOREIGN_TABLE:
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index dbc8f2d6c71cdbc37c92f2c2cf96dde239f6ab67..be263850cd354b3a478a1d582741e419fd24cf34 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1293,7 +1293,8 @@ heap_open(Oid relationId, LOCKMODE lockmode)
 
 	r = relation_open(relationId, lockmode);
 
-	if (r->rd_rel->relkind == RELKIND_INDEX)
+	if (r->rd_rel->relkind == RELKIND_INDEX ||
+		r->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an index",
@@ -1321,7 +1322,8 @@ heap_openrv(const RangeVar *relation, LOCKMODE lockmode)
 
 	r = relation_openrv(relation, lockmode);
 
-	if (r->rd_rel->relkind == RELKIND_INDEX)
+	if (r->rd_rel->relkind == RELKIND_INDEX ||
+		r->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an index",
@@ -1353,7 +1355,8 @@ heap_openrv_extended(const RangeVar *relation, LOCKMODE lockmode,
 
 	if (r)
 	{
-		if (r->rd_rel->relkind == RELKIND_INDEX)
+		if (r->rd_rel->relkind == RELKIND_INDEX ||
+			r->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("\"%s\" is an index",
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 1b61cd95151a257466d6c10742efc120658208db..91247f0fa59133d1a2bd69c4e17283e4196ad1e3 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -154,7 +154,8 @@ index_open(Oid relationId, LOCKMODE lockmode)
 
 	r = relation_open(relationId, lockmode);
 
-	if (r->rd_rel->relkind != RELKIND_INDEX)
+	if (r->rd_rel->relkind != RELKIND_INDEX &&
+		r->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is not an index",
diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 8c52846a92e34579e2391cdd17a7595b0bb4e737..dfd53fa054a692b34090f5dfdccbf881824e1c52 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -321,6 +321,7 @@ Boot_DeclareIndexStmt:
 					DefineIndex(relationId,
 								stmt,
 								$4,
+								InvalidOid,
 								false,
 								false,
 								false,
@@ -365,6 +366,7 @@ Boot_DeclareUniqueIndexStmt:
 					DefineIndex(relationId,
 								stmt,
 								$5,
+								InvalidOid,
 								false,
 								false,
 								false,
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index fac80612b80b73c08c4e9c446a4ea91d5f094ad1..50a2e2681b60f463b6341f2e9bd4344cf072f1f2 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -1824,7 +1824,8 @@ ExecGrant_Relation(InternalGrant *istmt)
 		pg_class_tuple = (Form_pg_class) GETSTRUCT(tuple);
 
 		/* Not sensible to grant on an index */
-		if (pg_class_tuple->relkind == RELKIND_INDEX)
+		if (pg_class_tuple->relkind == RELKIND_INDEX ||
+			pg_class_tuple->relkind == RELKIND_PARTITIONED_INDEX)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("\"%s\" is an index",
@@ -5405,7 +5406,8 @@ recordExtObjInitPriv(Oid objoid, Oid classoid)
 		pg_class_tuple = (Form_pg_class) GETSTRUCT(tuple);
 
 		/* Indexes don't have permissions */
-		if (pg_class_tuple->relkind == RELKIND_INDEX)
+		if (pg_class_tuple->relkind == RELKIND_INDEX ||
+			pg_class_tuple->relkind == RELKIND_PARTITIONED_INDEX)
 			return;
 
 		/* Composite types don't have permissions either */
@@ -5690,7 +5692,8 @@ removeExtObjInitPriv(Oid objoid, Oid classoid)
 		pg_class_tuple = (Form_pg_class) GETSTRUCT(tuple);
 
 		/* Indexes don't have permissions */
-		if (pg_class_tuple->relkind == RELKIND_INDEX)
+		if (pg_class_tuple->relkind == RELKIND_INDEX ||
+			pg_class_tuple->relkind == RELKIND_PARTITIONED_INDEX)
 			return;
 
 		/* Composite types don't have permissions either */
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 269111b4c160aa1a8341d906585992140b532ebb..be60270ea5ff21b21df5f742964b71e5ec735eda 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -582,6 +582,7 @@ findDependentObjects(const ObjectAddress *object,
 				/* FALL THRU */
 
 			case DEPENDENCY_INTERNAL:
+			case DEPENDENCY_INTERNAL_AUTO:
 
 				/*
 				 * This object is part of the internal implementation of
@@ -633,6 +634,14 @@ findDependentObjects(const ObjectAddress *object,
 				 * transform this deletion request into a delete of this
 				 * owning object.
 				 *
+				 * For INTERNAL_AUTO dependencies, we don't enforce this;
+				 * in other words, we don't follow the links back to the
+				 * owning object.
+				 */
+				if (foundDep->deptype == DEPENDENCY_INTERNAL_AUTO)
+					break;
+
+				/*
 				 * First, release caller's lock on this object and get
 				 * deletion lock on the owning object.  (We must release
 				 * caller's lock to avoid deadlock against a concurrent
@@ -675,6 +684,7 @@ findDependentObjects(const ObjectAddress *object,
 				/* And we're done here. */
 				systable_endscan(scan);
 				return;
+
 			case DEPENDENCY_PIN:
 
 				/*
@@ -762,6 +772,7 @@ findDependentObjects(const ObjectAddress *object,
 			case DEPENDENCY_AUTO_EXTENSION:
 				subflags = DEPFLAG_AUTO;
 				break;
+			case DEPENDENCY_INTERNAL_AUTO:
 			case DEPENDENCY_INTERNAL:
 				subflags = DEPFLAG_INTERNAL;
 				break;
@@ -1109,7 +1120,8 @@ doDeletion(const ObjectAddress *object, int flags)
 			{
 				char		relKind = get_rel_relkind(object->objectId);
 
-				if (relKind == RELKIND_INDEX)
+				if (relKind == RELKIND_INDEX ||
+					relKind == RELKIND_PARTITIONED_INDEX)
 				{
 					bool		concurrent = ((flags & PERFORM_DELETION_CONCURRENTLY) != 0);
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 089b7965f2a27d0077f7514900eb30e6ae261657..99f4d59863c1d2e536345daa1c0079825df2c858 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -294,6 +294,7 @@ heap_create(const char *relname,
 		case RELKIND_COMPOSITE_TYPE:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_PARTITIONED_INDEX:
 			create_storage = false;
 
 			/*
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 007b929a6faaef4ab03235d11ff58954c0ce0cfd..f0223416adf756e925fb9d893ba24007c81ca433 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -41,6 +41,8 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_constraint_fn.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_tablespace.h"
@@ -55,6 +57,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "parser/parser.h"
+#include "rewrite/rewriteManip.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
@@ -110,6 +113,7 @@ static void InitializeAttributeOids(Relation indexRelation,
 						int numatts, Oid indexoid);
 static void AppendAttributeTuples(Relation indexRelation, int numatts);
 static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
+					Oid parentIndexId,
 					IndexInfo *indexInfo,
 					Oid *collationOids,
 					Oid *classOids,
@@ -117,7 +121,8 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 					bool primary,
 					bool isexclusion,
 					bool immediate,
-					bool isvalid);
+					bool isvalid,
+					bool isready);
 static void index_update_stats(Relation rel,
 				   bool hasindex, bool isprimary,
 				   double reltuples);
@@ -563,6 +568,7 @@ AppendAttributeTuples(Relation indexRelation, int numatts)
 static void
 UpdateIndexRelation(Oid indexoid,
 					Oid heapoid,
+					Oid parentIndexOid,
 					IndexInfo *indexInfo,
 					Oid *collationOids,
 					Oid *classOids,
@@ -570,7 +576,8 @@ UpdateIndexRelation(Oid indexoid,
 					bool primary,
 					bool isexclusion,
 					bool immediate,
-					bool isvalid)
+					bool isvalid,
+					bool isready)
 {
 	int2vector *indkey;
 	oidvector  *indcollation;
@@ -644,8 +651,7 @@ UpdateIndexRelation(Oid indexoid,
 	values[Anum_pg_index_indisclustered - 1] = BoolGetDatum(false);
 	values[Anum_pg_index_indisvalid - 1] = BoolGetDatum(isvalid);
 	values[Anum_pg_index_indcheckxmin - 1] = BoolGetDatum(false);
-	/* we set isvalid and isready the same way */
-	values[Anum_pg_index_indisready - 1] = BoolGetDatum(isvalid);
+	values[Anum_pg_index_indisready - 1] = BoolGetDatum(isready);
 	values[Anum_pg_index_indislive - 1] = BoolGetDatum(true);
 	values[Anum_pg_index_indisreplident - 1] = BoolGetDatum(false);
 	values[Anum_pg_index_indkey - 1] = PointerGetDatum(indkey);
@@ -682,6 +688,8 @@ UpdateIndexRelation(Oid indexoid,
  * indexRelationId: normally, pass InvalidOid to let this routine
  *		generate an OID for the index.  During bootstrap this may be
  *		nonzero to specify a preselected OID.
+ * parentIndexRelid: if creating an index partition, the OID of the
+ *		parent index; otherwise InvalidOid.
  * relFileNode: normally, pass InvalidOid to get new storage.  May be
  *		nonzero to attach an existing valid build.
  * indexInfo: same info executor uses to insert into the index
@@ -707,6 +715,8 @@ UpdateIndexRelation(Oid indexoid,
  *		INDEX_CREATE_IF_NOT_EXISTS:
  *			do not throw an error if a relation with the same name
  *			already exists.
+ *		INDEX_CREATE_PARTITIONED:
+ *			create a partitioned index (table must be partitioned)
  * constr_flags: flags passed to index_constraint_create
  *		(only if INDEX_CREATE_ADD_CONSTRAINT is set)
  * allow_system_table_mods: allow table to be a system catalog
@@ -718,6 +728,7 @@ Oid
 index_create(Relation heapRelation,
 			 const char *indexRelationName,
 			 Oid indexRelationId,
+			 Oid parentIndexRelid,
 			 Oid relFileNode,
 			 IndexInfo *indexInfo,
 			 List *indexColNames,
@@ -743,12 +754,18 @@ index_create(Relation heapRelation,
 	int			i;
 	char		relpersistence;
 	bool		isprimary = (flags & INDEX_CREATE_IS_PRIMARY) != 0;
+	bool		invalid = (flags & INDEX_CREATE_INVALID) != 0;
 	bool		concurrent = (flags & INDEX_CREATE_CONCURRENT) != 0;
+	bool		partitioned = (flags & INDEX_CREATE_PARTITIONED) != 0;
+	char		relkind;
 
 	/* constraint flags can only be set when a constraint is requested */
 	Assert((constr_flags == 0) ||
 		   ((flags & INDEX_CREATE_ADD_CONSTRAINT) != 0));
+	/* partitioned indexes must never be "built" by themselves */
+	Assert(!partitioned || (flags & INDEX_CREATE_SKIP_BUILD));
 
+	relkind = partitioned ? RELKIND_PARTITIONED_INDEX : RELKIND_INDEX;
 	is_exclusion = (indexInfo->ii_ExclusionOps != NULL);
 
 	pg_class = heap_open(RelationRelationId, RowExclusiveLock);
@@ -866,9 +883,9 @@ index_create(Relation heapRelation,
 	}
 
 	/*
-	 * create the index relation's relcache entry and physical disk file. (If
-	 * we fail further down, it's the smgr's responsibility to remove the disk
-	 * file again.)
+	 * create the index relation's relcache entry and, if necessary, the
+	 * physical disk file. (If we fail further down, it's the smgr's
+	 * responsibility to remove the disk file again, if any.)
 	 */
 	indexRelation = heap_create(indexRelationName,
 								namespaceId,
@@ -876,7 +893,7 @@ index_create(Relation heapRelation,
 								indexRelationId,
 								relFileNode,
 								indexTupDesc,
-								RELKIND_INDEX,
+								relkind,
 								relpersistence,
 								shared_relation,
 								mapped_relation,
@@ -933,12 +950,18 @@ index_create(Relation heapRelation,
 	 *	  (Or, could define a rule to maintain the predicate) --Nels, Feb '92
 	 * ----------------
 	 */
-	UpdateIndexRelation(indexRelationId, heapRelationId, indexInfo,
+	UpdateIndexRelation(indexRelationId, heapRelationId, parentIndexRelid,
+						indexInfo,
 						collationObjectId, classObjectId, coloptions,
 						isprimary, is_exclusion,
 						(constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0,
+						!concurrent && !invalid,
 						!concurrent);
 
+	/* update pg_inherits, if needed */
+	if (OidIsValid(parentIndexRelid))
+		StoreSingleInheritance(indexRelationId, parentIndexRelid, 1);
+
 	/*
 	 * Register constraint and dependencies for the index.
 	 *
@@ -990,6 +1013,9 @@ index_create(Relation heapRelation,
 		else
 		{
 			bool		have_simple_col = false;
+			DependencyType	deptype;
+
+			deptype = OidIsValid(parentIndexRelid) ? DEPENDENCY_INTERNAL_AUTO : DEPENDENCY_AUTO;
 
 			/* Create auto dependencies on simply-referenced columns */
 			for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
@@ -1000,7 +1026,7 @@ index_create(Relation heapRelation,
 					referenced.objectId = heapRelationId;
 					referenced.objectSubId = indexInfo->ii_KeyAttrNumbers[i];
 
-					recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+					recordDependencyOn(&myself, &referenced, deptype);
 
 					have_simple_col = true;
 				}
@@ -1018,10 +1044,20 @@ index_create(Relation heapRelation,
 				referenced.objectId = heapRelationId;
 				referenced.objectSubId = 0;
 
-				recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+				recordDependencyOn(&myself, &referenced, deptype);
 			}
 		}
 
+		/* Store dependency on parent index, if any */
+		if (OidIsValid(parentIndexRelid))
+		{
+			referenced.classId = RelationRelationId;
+			referenced.objectId = parentIndexRelid;
+			referenced.objectSubId = 0;
+
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL_AUTO);
+		}
+
 		/* Store dependency on collations */
 		/* The default collation is pinned, so don't bother recording it */
 		for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
@@ -1567,9 +1603,10 @@ index_drop(Oid indexId, bool concurrent)
 	}
 
 	/*
-	 * Schedule physical removal of the files
+	 * Schedule physical removal of the files (if any)
 	 */
-	RelationDropStorage(userIndexRelation);
+	if (userIndexRelation->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
+		RelationDropStorage(userIndexRelation);
 
 	/*
 	 * Close and flush the index's relcache entry, to ensure relcache doesn't
@@ -1613,6 +1650,11 @@ index_drop(Oid indexId, bool concurrent)
 	 */
 	DeleteRelationTuple(indexId);
 
+	/*
+	 * fix INHERITS relation
+	 */
+	DeleteInheritsTuple(indexId, InvalidOid);
+
 	/*
 	 * We are presently too lazy to attempt to compute the new correct value
 	 * of relhasindex (the next VACUUM will fix it if necessary). So there is
@@ -1706,12 +1748,120 @@ BuildIndexInfo(Relation index)
 	ii->ii_BrokenHotChain = false;
 
 	/* set up for possible use by index AM */
+	ii->ii_Am = index->rd_rel->relam;
 	ii->ii_AmCache = NULL;
 	ii->ii_Context = CurrentMemoryContext;
 
 	return ii;
 }
 
+/*
+ * CompareIndexInfo
+ *		Return whether the properties of two indexes (in different tables)
+ *		indicate that they have the "same" definitions.
+ *
+ * Note: passing collations and opfamilies separately is a kludge.  Adding
+ * them to IndexInfo may result in better coding here and elsewhere.
+ *
+ * Use convert_tuples_by_name_map(index2, index1) to build the attmap.
+ */
+bool
+CompareIndexInfo(IndexInfo *info1, IndexInfo *info2,
+				 Oid *collations1, Oid *collations2,
+				 Oid *opfamilies1, Oid *opfamilies2,
+				 AttrNumber *attmap, int maplen)
+{
+	int		i;
+
+	if (info1->ii_Unique != info2->ii_Unique)
+		return false;
+
+	/* indexes are only equivalent if they have the same access method */
+	if (info1->ii_Am != info2->ii_Am)
+		return false;
+
+	/* and same number of attributes */
+	if (info1->ii_NumIndexAttrs != info2->ii_NumIndexAttrs)
+		return false;
+
+	/*
+	 * and columns match through the attribute map (actual attribute numbers
+	 * might differ!)  Note that this implies that index columns that are
+	 * expressions appear in the same positions.  We will next compare the
+	 * expressions themselves.
+	 */
+	for (i = 0; i < info1->ii_NumIndexAttrs; i++)
+	{
+		if (maplen < info2->ii_KeyAttrNumbers[i])
+			elog(ERROR, "incorrect attribute map");
+
+		if (attmap[info2->ii_KeyAttrNumbers[i] - 1] !=
+			info1->ii_KeyAttrNumbers[i])
+			return false;
+
+		if (collations1[i] != collations2[i])
+			return false;
+		if (opfamilies1[i] != opfamilies2[i])
+			return false;
+	}
+
+	/*
+	 * For expression indexes: either both are expression indexes, or neither
+	 * is; if they are, make sure the expressions match.
+	 */
+	if ((info1->ii_Expressions != NIL) != (info2->ii_Expressions != NIL))
+		return false;
+	if (info1->ii_Expressions != NIL)
+	{
+		bool	found_whole_row;
+		Node   *mapped;
+
+		mapped = map_variable_attnos((Node *) info2->ii_Expressions,
+									 1, 0, attmap, maplen,
+									 InvalidOid, &found_whole_row);
+		if (found_whole_row)
+		{
+			/*
+			 * we could throw an error here, but seems out of scope for this
+			 * routine.
+			 */
+			return false;
+		}
+
+		if (!equal(info1->ii_Expressions, mapped))
+			return false;
+	}
+
+	/* Partial index predicates must be identical, if they exist */
+	if ((info1->ii_Predicate == NULL) != (info2->ii_Predicate == NULL))
+		return false;
+	if (info1->ii_Predicate != NULL)
+	{
+		bool	found_whole_row;
+		Node   *mapped;
+
+		mapped = map_variable_attnos((Node *) info2->ii_Predicate,
+									 1, 0, attmap, maplen,
+									 InvalidOid, &found_whole_row);
+		if (found_whole_row)
+		{
+			/*
+			 * we could throw an error here, but seems out of scope for this
+			 * routine.
+			 */
+			return false;
+		}
+		if (!equal(info1->ii_Predicate, mapped))
+			return false;
+	}
+
+	/* No support currently for comparing exclusion indexes. */
+	if (info1->ii_ExclusionOps != NULL || info2->ii_ExclusionOps != NULL)
+		return false;
+
+	return true;
+}
+
 /* ----------------
  *		BuildSpeculativeIndexInfo
  *			Add extra state to IndexInfo record
@@ -1934,6 +2084,9 @@ index_update_stats(Relation rel,
 		elog(ERROR, "could not find tuple for relation %u", relid);
 	rd_rel = (Form_pg_class) GETSTRUCT(tuple);
 
+	/* Should this be a more comprehensive test? */
+	Assert(rd_rel->relkind != RELKIND_PARTITIONED_INDEX);
+
 	/* Apply required updates, if any, to copied tuple */
 
 	dirty = false;
@@ -3343,6 +3496,14 @@ reindex_index(Oid indexId, bool skip_constraint_checks, char persistence,
 	 */
 	iRel = index_open(indexId, AccessExclusiveLock);
 
+	/*
+	 * The case of reindexing partitioned tables and indexes is handled
+	 * differently by upper layers, so this case shouldn't arise.
+	 */
+	if (iRel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
+		elog(ERROR, "unsupported relation kind for index \"%s\"",
+			 RelationGetRelationName(iRel));
+
 	/*
 	 * Don't allow reindex on temp tables of other backends ... their local
 	 * buffer manager is not going to cope.
@@ -3542,6 +3703,22 @@ reindex_relation(Oid relid, int flags, int options)
 	 */
 	rel = heap_open(relid, ShareLock);
 
+	/*
+	 * This may be useful when implemented someday; but that day is not today.
+	 * For now, avoid erroring out when called in a multi-table context
+	 * (REINDEX SCHEMA) and happen to come across a partitioned table.  The
+	 * partitions may be reindexed on their own anyway.
+	 */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("REINDEX of partitioned tables is not yet implemented, skipping \"%s\"",
+						RelationGetRelationName(rel))));
+		heap_close(rel, ShareLock);
+		return false;
+	}
+
 	toast_relid = rel->rd_rel->reltoastrelid;
 
 	/*
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index bc999ca3c435a281ebddfd1a29f87a30311beb72..7576606c1b2b5186a21583faef12c00cbb6c52d9 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -1217,7 +1217,8 @@ get_relation_by_qualified_name(ObjectType objtype, List *object,
 	switch (objtype)
 	{
 		case OBJECT_INDEX:
-			if (relation->rd_rel->relkind != RELKIND_INDEX)
+			if (relation->rd_rel->relkind != RELKIND_INDEX &&
+				relation->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("\"%s\" is not an index",
@@ -3483,6 +3484,7 @@ getRelationDescription(StringInfo buffer, Oid relid)
 							 relname);
 			break;
 		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
 			appendStringInfo(buffer, _("index %s"),
 							 relname);
 			break;
@@ -3957,6 +3959,7 @@ getRelationTypeDescription(StringInfo buffer, Oid relid, int32 objectSubId)
 			appendStringInfoString(buffer, "table");
 			break;
 		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
 			appendStringInfoString(buffer, "index");
 			break;
 		case RELKIND_SEQUENCE:
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 9dfbe123b529c95489919541ca5387a8d304a786..2ea05f350b330ace23d214f33de8e656318f6e99 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -656,14 +656,19 @@ get_constraint_index(Oid constraintId)
 
 		/*
 		 * We assume any internal dependency of an index on the constraint
-		 * must be what we are looking for.  (The relkind test is just
-		 * paranoia; there shouldn't be any such dependencies otherwise.)
+		 * must be what we are looking for.
 		 */
 		if (deprec->classid == RelationRelationId &&
 			deprec->objsubid == 0 &&
-			deprec->deptype == DEPENDENCY_INTERNAL &&
-			get_rel_relkind(deprec->objid) == RELKIND_INDEX)
+			deprec->deptype == DEPENDENCY_INTERNAL)
 		{
+			char		relkind = get_rel_relkind(deprec->objid);
+
+			/* This is pure paranoia; there shouldn't be any such */
+			if (relkind != RELKIND_INDEX &&
+				relkind != RELKIND_PARTITIONED_INDEX)
+				break;
+
 			indexId = deprec->objid;
 			break;
 		}
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index b32d677347ffc3ea9ec170eb59b2766ef448284b..5a5beb9273efeec4b8dbc973afb3fc1fe9610d33 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -405,3 +405,83 @@ typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId)
 
 	return result;
 }
+
+/*
+ * Create a single pg_inherits row with the given data
+ */
+void
+StoreSingleInheritance(Oid relationId, Oid parentOid, int32 seqNumber)
+{
+	Datum		values[Natts_pg_inherits];
+	bool		nulls[Natts_pg_inherits];
+	HeapTuple	tuple;
+	Relation	inhRelation;
+
+	inhRelation = heap_open(InheritsRelationId, RowExclusiveLock);
+
+	/*
+	 * Make the pg_inherits entry
+	 */
+	values[Anum_pg_inherits_inhrelid - 1] = ObjectIdGetDatum(relationId);
+	values[Anum_pg_inherits_inhparent - 1] = ObjectIdGetDatum(parentOid);
+	values[Anum_pg_inherits_inhseqno - 1] = Int32GetDatum(seqNumber);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	tuple = heap_form_tuple(RelationGetDescr(inhRelation), values, nulls);
+
+	CatalogTupleInsert(inhRelation, tuple);
+
+	heap_freetuple(tuple);
+
+	heap_close(inhRelation, RowExclusiveLock);
+}
+
+/*
+ * DeleteInheritsTuple
+ *
+ * Delete pg_inherits tuples with the given inhrelid.  inhparent may be given
+ * as InvalidOid, in which case all tuples matching inhrelid are deleted;
+ * otherwise only delete tuples with the specified inhparent.
+ *
+ * Returns whether at least one row was deleted.
+ */
+bool
+DeleteInheritsTuple(Oid inhrelid, Oid inhparent)
+{
+	bool	found = false;
+	Relation	catalogRelation;
+	ScanKeyData key;
+	SysScanDesc scan;
+	HeapTuple	inheritsTuple;
+
+	/*
+	 * Find pg_inherits entries by inhrelid.
+	 */
+	catalogRelation = heap_open(InheritsRelationId, RowExclusiveLock);
+	ScanKeyInit(&key,
+				Anum_pg_inherits_inhrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(inhrelid));
+	scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId,
+							  true, NULL, 1, &key);
+
+	while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
+	{
+		Oid			parent;
+
+		/* Compare inhparent if it was given, and do the actual deletion. */
+		parent = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent;
+		if (!OidIsValid(inhparent) || parent == inhparent)
+		{
+			CatalogTupleDelete(catalogRelation, &inheritsTuple->t_self);
+			found = true;
+		}
+	}
+
+	/* Done */
+	systable_endscan(scan);
+	heap_close(catalogRelation, RowExclusiveLock);
+
+	return found;
+}
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 0b4b5631a1b05cbead553a09698072d50d1fb294..cf37011b73f95a791c8e2a298d7991a968a59a4f 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -315,6 +315,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	indexInfo->ii_ReadyForInserts = true;
 	indexInfo->ii_Concurrent = false;
 	indexInfo->ii_BrokenHotChain = false;
+	indexInfo->ii_Am = BTREE_AM_OID;
 	indexInfo->ii_AmCache = NULL;
 	indexInfo->ii_Context = CurrentMemoryContext;
 
@@ -328,6 +329,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	coloptions[1] = 0;
 
 	index_create(toast_rel, toast_idxname, toastIndexOid, InvalidOid,
+				 InvalidOid,
 				 indexInfo,
 				 list_make2("chunk_id", "chunk_seq"),
 				 BTREE_AM_OID,
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 9e6ba920086c52dd88b7aa0f569b999dcd9bb5d6..8118a39a7b159908a36113ded9a4aa82874c6832 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -23,7 +23,10 @@
 #include "catalog/catalog.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
+#include "catalog/partition.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_tablespace.h"
@@ -35,6 +38,7 @@
 #include "commands/tablespace.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/planner.h"
@@ -42,6 +46,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_func.h"
 #include "parser/parse_oper.h"
+#include "rewrite/rewriteManip.h"
 #include "storage/lmgr.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
@@ -77,6 +82,7 @@ static char *ChooseIndexNameAddition(List *colnames);
 static List *ChooseIndexColumnNames(List *indexElems);
 static void RangeVarCallbackForReindexIndex(const RangeVar *relation,
 								Oid relId, Oid oldRelId, void *arg);
+static void ReindexPartitionedIndex(Relation parentIdx);
 
 /*
  * CheckIndexCompatible
@@ -183,6 +189,7 @@ CheckIndexCompatible(Oid oldId,
 	indexInfo->ii_ExclusionOps = NULL;
 	indexInfo->ii_ExclusionProcs = NULL;
 	indexInfo->ii_ExclusionStrats = NULL;
+	indexInfo->ii_Am = accessMethodId;
 	indexInfo->ii_AmCache = NULL;
 	indexInfo->ii_Context = CurrentMemoryContext;
 	typeObjectId = (Oid *) palloc(numberOfAttributes * sizeof(Oid));
@@ -292,14 +299,15 @@ CheckIndexCompatible(Oid oldId,
  * 'stmt': IndexStmt describing the properties of the new index.
  * 'indexRelationId': normally InvalidOid, but during bootstrap can be
  *		nonzero to specify a preselected OID for the index.
+ * 'parentIndexId': the OID of the parent index; InvalidOid if not the child
+ *		of a partitioned index.
  * 'is_alter_table': this is due to an ALTER rather than a CREATE operation.
  * 'check_rights': check for CREATE rights in namespace and tablespace.  (This
  *		should be true except when ALTER is deleting/recreating an index.)
  * 'check_not_in_use': check for table not already in use in current session.
  *		This should be true unless caller is holding the table open, in which
  *		case the caller had better have checked it earlier.
- * 'skip_build': make the catalog entries but leave the index file empty;
- *		it will be filled later.
+ * 'skip_build': make the catalog entries but don't create the index files
  * 'quiet': suppress the NOTICE chatter ordinarily provided for constraints.
  *
  * Returns the object address of the created index.
@@ -308,6 +316,7 @@ ObjectAddress
 DefineIndex(Oid relationId,
 			IndexStmt *stmt,
 			Oid indexRelationId,
+			Oid parentIndexId,
 			bool is_alter_table,
 			bool check_rights,
 			bool check_not_in_use,
@@ -330,6 +339,7 @@ DefineIndex(Oid relationId,
 	IndexAmRoutine *amRoutine;
 	bool		amcanorder;
 	amoptions_function amoptions;
+	bool		partitioned;
 	Datum		reloptions;
 	int16	   *coloptions;
 	IndexInfo  *indexInfo;
@@ -382,23 +392,56 @@ DefineIndex(Oid relationId,
 	{
 		case RELKIND_RELATION:
 		case RELKIND_MATVIEW:
+		case RELKIND_PARTITIONED_TABLE:
 			/* OK */
 			break;
 		case RELKIND_FOREIGN_TABLE:
+			/*
+			 * Custom error message for FOREIGN TABLE since the term is close
+			 * to a regular table and can confuse the user.
+			 */
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot create index on foreign table \"%s\"",
 							RelationGetRelationName(rel))));
-		case RELKIND_PARTITIONED_TABLE:
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot create index on partitioned table \"%s\"",
-							RelationGetRelationName(rel))));
 		default:
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("\"%s\" is not a table or materialized view",
 							RelationGetRelationName(rel))));
+			break;
+	}
+
+	/*
+	 * Establish behavior for partitioned tables, and verify sanity of
+	 * parameters.
+	 *
+	 * We do not build an actual index in this case; we only create a few
+	 * catalog entries.  The actual indexes are built by recursing for each
+	 * partition.
+	 */
+	partitioned = rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
+	if (partitioned)
+	{
+		if (stmt->concurrent)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot create index on partitioned table \"%s\" concurrently",
+							RelationGetRelationName(rel))));
+		if (stmt->unique)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot create unique index on partitioned table \"%s\"",
+							RelationGetRelationName(rel))));
+		if (stmt->excludeOpNames)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot create exclusion constraints on partitioned table \"%s\"",
+							RelationGetRelationName(rel))));
+		if (stmt->primary || stmt->isconstraint)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot create constraints on partitioned tables")));
 	}
 
 	/*
@@ -574,6 +617,7 @@ DefineIndex(Oid relationId,
 	indexInfo->ii_ReadyForInserts = !stmt->concurrent;
 	indexInfo->ii_Concurrent = stmt->concurrent;
 	indexInfo->ii_BrokenHotChain = false;
+	indexInfo->ii_Am = accessMethodId;
 	indexInfo->ii_AmCache = NULL;
 	indexInfo->ii_Context = CurrentMemoryContext;
 
@@ -665,19 +709,24 @@ DefineIndex(Oid relationId,
 	/*
 	 * Make the catalog entries for the index, including constraints. This
 	 * step also actually builds the index, except if caller requested not to
-	 * or in concurrent mode, in which case it'll be done later.
+	 * or in concurrent mode, in which case it'll be done later, or
+	 * doing a partitioned index (because those don't have storage).
 	 */
 	flags = constr_flags = 0;
 	if (stmt->isconstraint)
 		flags |= INDEX_CREATE_ADD_CONSTRAINT;
-	if (skip_build || stmt->concurrent)
+	if (skip_build || stmt->concurrent || partitioned)
 		flags |= INDEX_CREATE_SKIP_BUILD;
 	if (stmt->if_not_exists)
 		flags |= INDEX_CREATE_IF_NOT_EXISTS;
 	if (stmt->concurrent)
 		flags |= INDEX_CREATE_CONCURRENT;
+	if (partitioned)
+		flags |= INDEX_CREATE_PARTITIONED;
 	if (stmt->primary)
 		flags |= INDEX_CREATE_IS_PRIMARY;
+	if (partitioned && stmt->relation && !stmt->relation->inh)
+		flags |= INDEX_CREATE_INVALID;
 
 	if (stmt->deferrable)
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
@@ -685,8 +734,8 @@ DefineIndex(Oid relationId,
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
 
 	indexRelationId =
-		index_create(rel, indexRelationName, indexRelationId, stmt->oldNode,
-					 indexInfo, indexColNames,
+		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
+					 stmt->oldNode, indexInfo, indexColNames,
 					 accessMethodId, tablespaceId,
 					 collationObjectId, classObjectId,
 					 coloptions, reloptions,
@@ -706,6 +755,160 @@ DefineIndex(Oid relationId,
 		CreateComments(indexRelationId, RelationRelationId, 0,
 					   stmt->idxcomment);
 
+	if (partitioned)
+	{
+		/*
+		 * Unless caller specified to skip this step (via ONLY), process
+		 * each partition to make sure they all contain a corresponding index.
+		 *
+		 * If we're called internally (no stmt->relation), recurse always.
+		 */
+		if (!stmt->relation || stmt->relation->inh)
+		{
+			PartitionDesc partdesc = RelationGetPartitionDesc(rel);
+			int			nparts = partdesc->nparts;
+			Oid		   *part_oids = palloc(sizeof(Oid) * nparts);
+			bool		invalidate_parent = false;
+			TupleDesc	parentDesc;
+			Oid		   *opfamOids;
+
+			memcpy(part_oids, partdesc->oids, sizeof(Oid) * nparts);
+
+			parentDesc = CreateTupleDescCopy(RelationGetDescr(rel));
+			opfamOids = palloc(sizeof(Oid) * numberOfAttributes);
+			for (i = 0; i < numberOfAttributes; i++)
+				opfamOids[i] = get_opclass_family(classObjectId[i]);
+
+			heap_close(rel, NoLock);
+
+			/*
+			 * For each partition, scan all existing indexes; if one matches
+			 * our index definition and is not already attached to some other
+			 * parent index, attach it to the one we just created.
+			 *
+			 * If none matches, build a new index by calling ourselves
+			 * recursively with the same options (except for the index name).
+			 */
+			for (i = 0; i < nparts; i++)
+			{
+				Oid		childRelid = part_oids[i];
+				Relation childrel;
+				List   *childidxs;
+				ListCell *cell;
+				AttrNumber *attmap;
+				bool	found = false;
+				int		maplen;
+
+				childrel = heap_open(childRelid, lockmode);
+				childidxs = RelationGetIndexList(childrel);
+				attmap =
+					convert_tuples_by_name_map(RelationGetDescr(childrel),
+											   parentDesc,
+											   gettext_noop("could not convert row type"));
+				maplen = parentDesc->natts;
+
+
+				foreach(cell, childidxs)
+				{
+					Oid			cldidxid = lfirst_oid(cell);
+					Relation	cldidx;
+					IndexInfo  *cldIdxInfo;
+
+					/* this index is already partition of another one */
+					if (has_superclass(cldidxid))
+						continue;
+
+					cldidx = index_open(cldidxid, lockmode);
+					cldIdxInfo = BuildIndexInfo(cldidx);
+					if (CompareIndexInfo(cldIdxInfo, indexInfo,
+										 cldidx->rd_indcollation,
+										 collationObjectId,
+										 cldidx->rd_opfamily,
+										 opfamOids,
+										 attmap, maplen))
+					{
+						/*
+						 * Found a match.  Attach index to parent and we're
+						 * done, but keep lock till commit.
+						 */
+						IndexSetParentIndex(cldidx, indexRelationId);
+
+						if (!IndexIsValid(cldidx->rd_index))
+							invalidate_parent = true;
+
+						found = true;
+						index_close(cldidx, NoLock);
+						break;
+					}
+
+					index_close(cldidx, lockmode);
+				}
+
+				list_free(childidxs);
+				heap_close(childrel, NoLock);
+
+				/*
+				 * If no matching index was found, create our own.
+				 */
+				if (!found)
+				{
+					IndexStmt  *childStmt = copyObject(stmt);
+					bool		found_whole_row;
+
+					childStmt->whereClause =
+						map_variable_attnos(stmt->whereClause, 1, 0,
+											attmap, maplen,
+											InvalidOid, &found_whole_row);
+					if (found_whole_row)
+						elog(ERROR, "cannot convert whole-row table reference");
+
+					childStmt->idxname = NULL;
+					childStmt->relationId = childRelid;
+					DefineIndex(childRelid, childStmt,
+								InvalidOid,			/* no predefined OID */
+								indexRelationId,	/* this is our child */
+								false, check_rights, check_not_in_use,
+								false, quiet);
+				}
+
+				pfree(attmap);
+			}
+
+			/*
+			 * The pg_index row we inserted for this index was marked
+			 * indisvalid=true.  But if we attached an existing index that
+			 * is invalid, this is incorrect, so update our row to
+			 * invalid too.
+			 */
+			if (invalidate_parent)
+			{
+				Relation	pg_index = heap_open(IndexRelationId, RowExclusiveLock);
+				HeapTuple	tup,
+							newtup;
+
+				tup = SearchSysCache1(INDEXRELID,
+									  ObjectIdGetDatum(indexRelationId));
+				if (!tup)
+					elog(ERROR, "cache lookup failed for index %u",
+						 indexRelationId);
+				newtup = heap_copytuple(tup);
+				((Form_pg_index) GETSTRUCT(newtup))->indisvalid = false;
+				CatalogTupleUpdate(pg_index, &tup->t_self, newtup);
+				ReleaseSysCache(tup);
+				heap_close(pg_index, RowExclusiveLock);
+				heap_freetuple(newtup);
+			}
+		}
+		else
+			heap_close(rel, NoLock);
+
+		/*
+		 * Indexes on partitioned tables are not themselves built, so we're
+		 * done here.
+		 */
+		return address;
+	}
+
 	if (!stmt->concurrent)
 	{
 		/* Close the heap and we're done, in the non-concurrent case */
@@ -1765,7 +1968,7 @@ ChooseIndexColumnNames(List *indexElems)
  * ReindexIndex
  *		Recreate a specific index.
  */
-Oid
+void
 ReindexIndex(RangeVar *indexRelation, int options)
 {
 	Oid			indOid;
@@ -1788,12 +1991,17 @@ ReindexIndex(RangeVar *indexRelation, int options)
 	 * lock on the index.
 	 */
 	irel = index_open(indOid, NoLock);
+
+	if (irel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
+	{
+		ReindexPartitionedIndex(irel);
+		return;
+	}
+
 	persistence = irel->rd_rel->relpersistence;
 	index_close(irel, NoLock);
 
 	reindex_index(indOid, false, persistence, options);
-
-	return indOid;
 }
 
 /*
@@ -1832,7 +2040,8 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
 	relkind = get_rel_relkind(relId);
 	if (!relkind)
 		return;
-	if (relkind != RELKIND_INDEX)
+	if (relkind != RELKIND_INDEX &&
+		relkind != RELKIND_PARTITIONED_INDEX)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is not an index", relation->relname)));
@@ -1976,6 +2185,12 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 		/*
 		 * Only regular tables and matviews can have indexes, so ignore any
 		 * other kind of relation.
+		 *
+		 * It is tempting to also consider partitioned tables here, but that
+		 * has the problem that if the children are in the same schema, they
+		 * would be processed twice.  Maybe we could have a separate list of
+		 * partitioned tables, and expand that afterwards into relids,
+		 * ignoring any duplicates.
 		 */
 		if (classtuple->relkind != RELKIND_RELATION &&
 			classtuple->relkind != RELKIND_MATVIEW)
@@ -2038,3 +2253,155 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 
 	MemoryContextDelete(private_context);
 }
+
+/*
+ *	ReindexPartitionedIndex
+ *		Reindex each child of the given partitioned index.
+ *
+ * Not yet implemented.
+ */
+static void
+ReindexPartitionedIndex(Relation parentIdx)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("REINDEX is not yet implemented for partitioned indexes")));
+}
+
+/*
+ * Insert or delete an appropriate pg_inherits tuple to make the given index
+ * be a partition of the indicated parent index.
+ *
+ * This also corrects the pg_depend information for the affected index.
+ */
+void
+IndexSetParentIndex(Relation partitionIdx, Oid parentOid)
+{
+	Relation	pg_inherits;
+	ScanKeyData	key[2];
+	SysScanDesc	scan;
+	Oid			partRelid = RelationGetRelid(partitionIdx);
+	HeapTuple	tuple;
+	bool		fix_dependencies;
+
+	/* Make sure this is an index */
+	Assert(partitionIdx->rd_rel->relkind == RELKIND_INDEX ||
+		   partitionIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX);
+
+	/*
+	 * Scan pg_inherits for rows linking our index to some parent.
+	 */
+	pg_inherits = relation_open(InheritsRelationId, RowExclusiveLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_inherits_inhrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(partRelid));
+	ScanKeyInit(&key[1],
+				Anum_pg_inherits_inhseqno,
+				BTEqualStrategyNumber, F_INT4EQ,
+				Int32GetDatum(1));
+	scan = systable_beginscan(pg_inherits, InheritsRelidSeqnoIndexId, true,
+							  NULL, 2, key);
+	tuple = systable_getnext(scan);
+
+	if (!HeapTupleIsValid(tuple))
+	{
+		if (parentOid == InvalidOid)
+		{
+			/*
+			 * No pg_inherits row, and no parent wanted: nothing to do in
+			 * this case.
+			 */
+			fix_dependencies = false;
+		}
+		else
+		{
+			Datum	values[Natts_pg_inherits];
+			bool	isnull[Natts_pg_inherits];
+
+			/*
+			 * No pg_inherits row exists, and we want a parent for this index,
+			 * so insert it.
+			 */
+			values[Anum_pg_inherits_inhrelid - 1] = ObjectIdGetDatum(partRelid);
+			values[Anum_pg_inherits_inhparent - 1] =
+				ObjectIdGetDatum(parentOid);
+			values[Anum_pg_inherits_inhseqno - 1] = Int32GetDatum(1);
+			memset(isnull, false, sizeof(isnull));
+
+			tuple = heap_form_tuple(RelationGetDescr(pg_inherits),
+									values, isnull);
+			CatalogTupleInsert(pg_inherits, tuple);
+
+			fix_dependencies = true;
+		}
+	}
+	else
+	{
+		Form_pg_inherits	inhForm = (Form_pg_inherits) GETSTRUCT(tuple);
+
+		if (parentOid == InvalidOid)
+		{
+			/*
+			 * There exists a pg_inherits row, which we want to clear; do so.
+			 */
+			CatalogTupleDelete(pg_inherits, &tuple->t_self);
+			fix_dependencies = true;
+		}
+		else
+		{
+			/*
+			 * A pg_inherits row exists.  If it's the same we want, then we're
+			 * good; if it differs, that amounts to a corrupt catalog and
+			 * should not happen.
+			 */
+			if (inhForm->inhparent != parentOid)
+			{
+				/* unexpected: we should not get called in this case */
+				elog(ERROR, "bogus pg_inherit row: inhrelid %u inhparent %u",
+					 inhForm->inhrelid, inhForm->inhparent);
+			}
+
+			/* already in the right state */
+			fix_dependencies = false;
+		}
+	}
+
+	/* done with pg_inherits */
+	systable_endscan(scan);
+	relation_close(pg_inherits, RowExclusiveLock);
+
+	if (fix_dependencies)
+	{
+		ObjectAddress partIdx;
+
+		/*
+		 * Insert/delete pg_depend rows.  If setting a parent, add an
+		 * INTERNAL_AUTO dependency to the parent index; if making standalone,
+		 * remove all existing rows and put back the regular dependency on the
+		 * table.
+		 */
+		ObjectAddressSet(partIdx, RelationRelationId, partRelid);
+
+		if (OidIsValid(parentOid))
+		{
+			ObjectAddress	parentIdx;
+
+			ObjectAddressSet(parentIdx, RelationRelationId, parentOid);
+			recordDependencyOn(&partIdx, &parentIdx, DEPENDENCY_INTERNAL_AUTO);
+		}
+		else
+		{
+			ObjectAddress	partitionTbl;
+
+			ObjectAddressSet(partitionTbl, RelationRelationId,
+							 partitionIdx->rd_index->indrelid);
+
+			deleteDependencyRecordsForClass(RelationRelationId, partRelid,
+											RelationRelationId,
+											DEPENDENCY_INTERNAL_AUTO);
+
+			recordDependencyOn(&partIdx, &partitionTbl, DEPENDENCY_AUTO);
+		}
+	}
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59806349cc7fd47ddf912c7f2229f27130ed1ec6..57ee112653f1084333c88d946d5f08f31e80f9b4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -266,6 +266,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = {
 		gettext_noop("table \"%s\" does not exist, skipping"),
 		gettext_noop("\"%s\" is not a table"),
 	gettext_noop("Use DROP TABLE to remove a table.")},
+	{RELKIND_PARTITIONED_INDEX,
+		ERRCODE_UNDEFINED_OBJECT,
+		gettext_noop("index \"%s\" does not exist"),
+		gettext_noop("index \"%s\" does not exist, skipping"),
+		gettext_noop("\"%s\" is not an index"),
+	gettext_noop("Use DROP INDEX to remove an index.")},
 	{'\0', 0, NULL, NULL, NULL, NULL}
 };
 
@@ -284,6 +290,7 @@ struct DropRelationCallbackState
 #define		ATT_INDEX				0x0008
 #define		ATT_COMPOSITE_TYPE		0x0010
 #define		ATT_FOREIGN_TABLE		0x0020
+#define		ATT_PARTITIONED_INDEX	0x0040
 
 /*
  * Partition tables are expected to be dropped when the parent partitioned
@@ -475,11 +482,17 @@ static void CreateInheritance(Relation child_rel, Relation parent_rel);
 static void RemoveInheritance(Relation child_rel, Relation parent_rel);
 static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
 					  PartitionCmd *cmd);
+static void AttachPartitionEnsureIndexes(Relation rel, Relation attachrel);
 static void ValidatePartitionConstraints(List **wqueue, Relation scanrel,
 							 List *scanrel_children,
 							 List *partConstraint,
 							 bool validate_default);
 static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
+static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
+						 RangeVar *name);
+static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
+static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
+					  Relation partitionTbl);
 
 
 /* ----------------------------------------------------------------
@@ -897,6 +910,53 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
 		StorePartitionKey(rel, strategy, partnatts, partattrs, partexprs,
 						  partopclass, partcollation);
+
+		/* make it all visible */
+		CommandCounterIncrement();
+	}
+
+	/*
+	 * If we're creating a partition, create now all the indexes defined in
+	 * the parent.  We can't do it earlier, because DefineIndex wants to know
+	 * the partition key which we just stored.
+	 */
+	if (stmt->partbound)
+	{
+		Oid			parentId = linitial_oid(inheritOids);
+		Relation	parent;
+		List	   *idxlist;
+		ListCell   *cell;
+
+		/* Already have strong enough lock on the parent */
+		parent = heap_open(parentId, NoLock);
+		idxlist = RelationGetIndexList(parent);
+
+		/*
+		 * For each index in the parent table, create one in the partition
+		 */
+		foreach(cell, idxlist)
+		{
+			Relation	idxRel = index_open(lfirst_oid(cell), AccessShareLock);
+			AttrNumber *attmap;
+			IndexStmt  *idxstmt;
+
+			attmap = convert_tuples_by_name_map(RelationGetDescr(rel),
+												RelationGetDescr(parent),
+												gettext_noop("could not convert row type"));
+			idxstmt =
+				generateClonedIndexStmt(NULL, RelationGetRelid(rel), idxRel,
+										attmap, RelationGetDescr(rel)->natts);
+			DefineIndex(RelationGetRelid(rel),
+						idxstmt,
+						InvalidOid,
+						RelationGetRelid(idxRel),
+						false, false, false, false, false);
+
+			index_close(idxRel, AccessShareLock);
+		}
+
+		list_free(idxlist);
+		heap_close(parent, NoLock);
 	}
 
 	/*
@@ -1179,10 +1239,13 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
 	 * but RemoveRelations() can only pass one relkind for a given relation.
 	 * It chooses RELKIND_RELATION for both regular and partitioned tables.
 	 * That means we must be careful before giving the wrong type error when
-	 * the relation is RELKIND_PARTITIONED_TABLE.
+	 * the relation is RELKIND_PARTITIONED_TABLE.  An equivalent problem
+	 * exists with indexes.
 	 */
 	if (classform->relkind == RELKIND_PARTITIONED_TABLE)
 		expected_relkind = RELKIND_RELATION;
+	else if (classform->relkind == RELKIND_PARTITIONED_INDEX)
+		expected_relkind = RELKIND_INDEX;
 	else
 		expected_relkind = classform->relkind;
 
@@ -1210,7 +1273,8 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
 	 * we do it the other way around.  No error if we don't find a pg_index
 	 * entry, though --- the relation may have been dropped.
 	 */
-	if (relkind == RELKIND_INDEX && relOid != oldRelOid)
+	if ((relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX) &&
+		relOid != oldRelOid)
 	{
 		state->heapOid = IndexGetRelation(relOid, true);
 		if (OidIsValid(state->heapOid))
@@ -2396,27 +2460,11 @@ StoreCatalogInheritance1(Oid relationId, Oid parentOid,
 						 int32 seqNumber, Relation inhRelation,
 						 bool child_is_partition)
 {
-	TupleDesc	desc = RelationGetDescr(inhRelation);
-	Datum		values[Natts_pg_inherits];
-	bool		nulls[Natts_pg_inherits];
 	ObjectAddress childobject,
 				parentobject;
-	HeapTuple	tuple;
-
-	/*
-	 * Make the pg_inherits entry
-	 */
-	values[Anum_pg_inherits_inhrelid - 1] = ObjectIdGetDatum(relationId);
-	values[Anum_pg_inherits_inhparent - 1] = ObjectIdGetDatum(parentOid);
-	values[Anum_pg_inherits_inhseqno - 1] = Int32GetDatum(seqNumber);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(desc, values, nulls);
-
-	CatalogTupleInsert(inhRelation, tuple);
-
-	heap_freetuple(tuple);
+	/* store the pg_inherits row */
+	StoreSingleInheritance(relationId, parentOid, seqNumber);
 
 	/*
 	 * Store a dependency too
@@ -2540,6 +2588,7 @@ renameatt_check(Oid myrelid, Form_pg_class classform, bool recursing)
 		relkind != RELKIND_MATVIEW &&
 		relkind != RELKIND_COMPOSITE_TYPE &&
 		relkind != RELKIND_INDEX &&
+		relkind != RELKIND_PARTITIONED_INDEX &&
 		relkind != RELKIND_FOREIGN_TABLE &&
 		relkind != RELKIND_PARTITIONED_TABLE)
 		ereport(ERROR,
@@ -3019,7 +3068,8 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal)
 	/*
 	 * Also rename the associated constraint, if any.
 	 */
-	if (targetrelation->rd_rel->relkind == RELKIND_INDEX)
+	if (targetrelation->rd_rel->relkind == RELKIND_INDEX ||
+		targetrelation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 	{
 		Oid			constraintId = get_index_constraint(myrelid);
 
@@ -3073,6 +3123,7 @@ CheckTableNotInUse(Relation rel, const char *stmt)
 						stmt, RelationGetRelationName(rel))));
 
 	if (rel->rd_rel->relkind != RELKIND_INDEX &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX &&
 		AfterTriggerPendingOnRel(RelationGetRelid(rel)))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_IN_USE),
@@ -3764,6 +3815,10 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			pass = AT_PASS_MISC;
 			break;
 		case AT_AttachPartition:
+			ATSimplePermissions(rel, ATT_TABLE | ATT_PARTITIONED_INDEX);
+			/* No command-specific prep needed */
+			pass = AT_PASS_MISC;
+			break;
 		case AT_DetachPartition:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* No command-specific prep needed */
@@ -4112,9 +4167,15 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
 		case AT_AttachPartition:
-			ATExecAttachPartition(wqueue, rel, (PartitionCmd *) cmd->def);
+			if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+				ATExecAttachPartition(wqueue, rel, (PartitionCmd *) cmd->def);
+			else
+				ATExecAttachPartitionIdx(wqueue, rel,
+										 ((PartitionCmd *) cmd->def)->name);
 			break;
 		case AT_DetachPartition:
+			/* ATPrepCmd ensures it must be a table */
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
 			ATExecDetachPartition(rel, ((PartitionCmd *) cmd->def)->name);
 			break;
 		default:				/* oops */
@@ -4148,9 +4209,13 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode)
 	{
 		AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
 
-		/* Foreign tables have no storage, nor do partitioned tables. */
+		/*
+		 * Foreign tables have no storage, nor do partitioned tables and
+		 * indexes.
+		 */
 		if (tab->relkind == RELKIND_FOREIGN_TABLE ||
-			tab->relkind == RELKIND_PARTITIONED_TABLE)
+			tab->relkind == RELKIND_PARTITIONED_TABLE ||
+			tab->relkind == RELKIND_PARTITIONED_INDEX)
 			continue;
 
 		/*
@@ -4752,6 +4817,9 @@ ATSimplePermissions(Relation rel, int allowed_targets)
 		case RELKIND_INDEX:
 			actual_target = ATT_INDEX;
 			break;
+		case RELKIND_PARTITIONED_INDEX:
+			actual_target = ATT_PARTITIONED_INDEX;
+			break;
 		case RELKIND_COMPOSITE_TYPE:
 			actual_target = ATT_COMPOSITE_TYPE;
 			break;
@@ -6194,6 +6262,7 @@ ATPrepSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 	if (rel->rd_rel->relkind != RELKIND_RELATION &&
 		rel->rd_rel->relkind != RELKIND_MATVIEW &&
 		rel->rd_rel->relkind != RELKIND_INDEX &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX &&
 		rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
 		rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
 		ereport(ERROR,
@@ -6205,7 +6274,9 @@ ATPrepSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 	 * We allow referencing columns by numbers only for indexes, since table
 	 * column numbers could contain gaps if columns are later dropped.
 	 */
-	if (rel->rd_rel->relkind != RELKIND_INDEX && !colName)
+	if (rel->rd_rel->relkind != RELKIND_INDEX &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX &&
+		!colName)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot refer to non-index column by number")));
@@ -6283,7 +6354,8 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	if (rel->rd_rel->relkind == RELKIND_INDEX &&
+	if ((rel->rd_rel->relkind == RELKIND_INDEX ||
+		 rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX) &&
 		rel->rd_index->indkey.values[attnum - 1] != 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -6736,6 +6808,7 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
 	address = DefineIndex(RelationGetRelid(rel),
 						  stmt,
 						  InvalidOid,	/* no predefined OID */
+						  InvalidOid,	/* no parent index */
 						  true, /* is_alter_table */
 						  check_rights,
 						  false,	/* check_not_in_use - we did it already */
@@ -9139,7 +9212,8 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				{
 					char		relKind = get_rel_relkind(foundObject.objectId);
 
-					if (relKind == RELKIND_INDEX)
+					if (relKind == RELKIND_INDEX ||
+						relKind == RELKIND_PARTITIONED_INDEX)
 					{
 						Assert(foundObject.objectSubId == 0);
 						if (!list_member_oid(tab->changedIndexOids, foundObject.objectId))
@@ -9982,6 +10056,15 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 				newOwnerId = tuple_class->relowner;
 			}
 			break;
+		case RELKIND_PARTITIONED_INDEX:
+			if (recursing)
+				break;
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot change owner of index \"%s\"",
+							NameStr(tuple_class->relname)),
+					 errhint("Change the ownership of the index's table, instead.")));
+			break;
 		case RELKIND_SEQUENCE:
 			if (!recursing &&
 				tuple_class->relowner != newOwnerId)
@@ -10103,6 +10186,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 		 */
 		if (tuple_class->relkind != RELKIND_COMPOSITE_TYPE &&
 			tuple_class->relkind != RELKIND_INDEX &&
+			tuple_class->relkind != RELKIND_PARTITIONED_INDEX &&
 			tuple_class->relkind != RELKIND_TOASTVALUE)
 			changeDependencyOnOwner(RelationRelationId, relationOid,
 									newOwnerId);
@@ -10110,7 +10194,8 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 		/*
 		 * Also change the ownership of the table's row type, if it has one
 		 */
-		if (tuple_class->relkind != RELKIND_INDEX)
+		if (tuple_class->relkind != RELKIND_INDEX &&
+			tuple_class->relkind != RELKIND_PARTITIONED_INDEX)
 			AlterTypeOwnerInternal(tuple_class->reltype, newOwnerId);
 
 		/*
@@ -10119,6 +10204,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 		 * relation, as well as its toast table (if it has one).
 		 */
 		if (tuple_class->relkind == RELKIND_RELATION ||
+			tuple_class->relkind == RELKIND_PARTITIONED_TABLE ||
 			tuple_class->relkind == RELKIND_MATVIEW ||
 			tuple_class->relkind == RELKIND_TOASTVALUE)
 		{
@@ -10427,6 +10513,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 			(void) view_reloptions(newOptions, true);
 			break;
 		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
 			(void) index_reloptions(rel->rd_amroutine->amoptions, newOptions, true);
 			break;
 		default:
@@ -10839,7 +10926,8 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)
 			 relForm->relkind != RELKIND_RELATION &&
 			 relForm->relkind != RELKIND_PARTITIONED_TABLE) ||
 			(stmt->objtype == OBJECT_INDEX &&
-			 relForm->relkind != RELKIND_INDEX) ||
+			 relForm->relkind != RELKIND_INDEX &&
+			 relForm->relkind != RELKIND_PARTITIONED_INDEX) ||
 			(stmt->objtype == OBJECT_MATVIEW &&
 			 relForm->relkind != RELKIND_MATVIEW))
 			continue;
@@ -11633,45 +11721,18 @@ RemoveInheritance(Relation child_rel, Relation parent_rel)
 	Relation	catalogRelation;
 	SysScanDesc scan;
 	ScanKeyData key[3];
-	HeapTuple	inheritsTuple,
-				attributeTuple,
+	HeapTuple	attributeTuple,
 				constraintTuple;
 	List	   *connames;
-	bool		found = false;
+	bool		found;
 	bool		child_is_partition = false;
 
 	/* If parent_rel is a partitioned table, child_rel must be a partition */
 	if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 		child_is_partition = true;
 
-	/*
-	 * Find and destroy the pg_inherits entry linking the two, or error out if
-	 * there is none.
-	 */
-	catalogRelation = heap_open(InheritsRelationId, RowExclusiveLock);
-	ScanKeyInit(&key[0],
-				Anum_pg_inherits_inhrelid,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(RelationGetRelid(child_rel)));
-	scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId,
-							  true, NULL, 1, key);
-
-	while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
-	{
-		Oid			inhparent;
-
-		inhparent = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent;
-		if (inhparent == RelationGetRelid(parent_rel))
-		{
-			CatalogTupleDelete(catalogRelation, &inheritsTuple->t_self);
-			found = true;
-			break;
-		}
-	}
-
-	systable_endscan(scan);
-	heap_close(catalogRelation, RowExclusiveLock);
-
+	found = DeleteInheritsTuple(RelationGetRelid(child_rel),
+								RelationGetRelid(parent_rel));
 	if (!found)
 	{
 		if (child_is_partition)
@@ -13226,7 +13287,8 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is not a composite type", rv->relname)));
 
-	if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX
+	if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX &&
+		relkind != RELKIND_PARTITIONED_INDEX
 		&& !IsA(stmt, RenameStmt))
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -13946,6 +14008,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
 	/* Update the pg_class entry. */
 	StorePartitionBound(attachrel, rel, cmd->bound);
 
+	/* Ensure there exists a correct set of indexes in the partition. */
+	AttachPartitionEnsureIndexes(rel, attachrel);
+
 	/*
 	 * Generate partition constraint from the partition bound specification.
 	 * If the parent itself is a partition, make sure to include its
@@ -14015,6 +14080,127 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
 	return address;
 }
 
+/*
+ * AttachPartitionEnsureIndexes
+ *		subroutine for ATExecAttachPartition to create/match indexes
+ *
+ * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH
+ * PARTITION: every partition must have an index attached to each index on the
+ * partitioned table.
+ */
+static void
+AttachPartitionEnsureIndexes(Relation rel, Relation attachrel)
+{
+	List	   *idxes;
+	List	   *attachRelIdxs;
+	Relation   *attachrelIdxRels;
+	IndexInfo **attachInfos;
+	int			i;
+	ListCell   *cell;
+	MemoryContext cxt;
+	MemoryContext oldcxt;
+
+	cxt = AllocSetContextCreate(CurrentMemoryContext,
+								"AttachPartitionEnsureIndexes",
+								ALLOCSET_DEFAULT_SIZES);
+	oldcxt = MemoryContextSwitchTo(cxt);
+
+	idxes = RelationGetIndexList(rel);
+	attachRelIdxs = RelationGetIndexList(attachrel);
+	attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs));
+	attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs));
+
+	/* Build arrays of all existing indexes and their IndexInfos */
+	i = 0;
+	foreach(cell, attachRelIdxs)
+	{
+		Oid			cldIdxId = lfirst_oid(cell);
+
+		attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock);
+		attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]);
+		i++;
+	}
+
+	/*
+	 * For each index on the partitioned table, find a matching one in the
+	 * partition-to-be; if one is not found, create one.
+	 */
+	foreach(cell, idxes)
+	{
+		Oid			idx = lfirst_oid(cell);
+		Relation	idxRel = index_open(idx, AccessShareLock);
+		IndexInfo  *info;
+		AttrNumber *attmap;
+		bool		found = false;
+
+		/*
+		 * Ignore indexes in the partitioned table other than partitioned
+		 * indexes.
+		 */
+		if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
+		{
+			index_close(idxRel, AccessShareLock);
+			continue;
+		}
+
+		/* construct an indexinfo to compare existing indexes against */
+		info = BuildIndexInfo(idxRel);
+		attmap = convert_tuples_by_name_map(RelationGetDescr(attachrel),
+											RelationGetDescr(rel),
+											gettext_noop("could not convert row type"));
+
+		/*
+		 * Scan the list of existing indexes in the partition-to-be, and mark
+		 * the first matching, unattached one we find, if any, as partition of
+		 * the parent index.  If we find one, we're done.
+		 */
+		for (i = 0; i < list_length(attachRelIdxs); i++)
+		{
+			/* does this index have a parent?  if so, can't use it */
+			if (has_superclass(RelationGetRelid(attachrelIdxRels[i])))
+				continue;
+
+			if (CompareIndexInfo(attachInfos[i], info,
+								 attachrelIdxRels[i]->rd_indcollation,
+								 idxRel->rd_indcollation,
+								 attachrelIdxRels[i]->rd_opfamily,
+								 idxRel->rd_opfamily,
+								 attmap,
+								 RelationGetDescr(rel)->natts))
+			{
+				/* bingo. */
+				IndexSetParentIndex(attachrelIdxRels[i], idx);
+				found = true;
+				break;
+			}
+		}
+
+		/*
+		 * If no suitable index was found in the partition-to-be, create one
+		 * now.
+		 */
+		if (!found)
+		{
+			IndexStmt  *stmt;
+
+			stmt = generateClonedIndexStmt(NULL, RelationGetRelid(attachrel),
+										   idxRel, attmap,
+										   RelationGetDescr(rel)->natts);
+			DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid,
+						RelationGetRelid(idxRel),
+						false, false, false, false, false);
+		}
+
+		index_close(idxRel, AccessShareLock);
+	}
+
+	/* Clean up. */
+	for (i = 0; i < list_length(attachRelIdxs); i++)
+		index_close(attachrelIdxRels[i], AccessShareLock);
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(cxt);
+}
+
 /*
  * ALTER TABLE DETACH PARTITION
  *
@@ -14033,6 +14219,8 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 				new_repl[Natts_pg_class];
 	ObjectAddress address;
 	Oid			defaultPartOid;
+	List	   *indexes;
+	ListCell   *cell;
 
 	/*
 	 * We must lock the default partition, because detaching this partition
@@ -14094,6 +14282,24 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 		}
 	}
 
+	/* detach indexes too */
+	indexes = RelationGetIndexList(partRel);
+	foreach(cell, indexes)
+	{
+		Oid			idxid = lfirst_oid(cell);
+		Relation	idx;
+
+		if (!has_superclass(idxid))
+			continue;
+
+		Assert((IndexGetRelation(get_partition_parent(idxid), false) ==
+			   RelationGetRelid(rel)));
+
+		idx = index_open(idxid, AccessExclusiveLock);
+		IndexSetParentIndex(idx, InvalidOid);
+		relation_close(idx, AccessExclusiveLock);
+	}
+
 	/*
 	 * Invalidate the parent's relcache so that the partition is no longer
 	 * included in its partition descriptor.
@@ -14107,3 +14313,328 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
 
 	return address;
 }
+
+/*
+ * Before acquiring lock on an index, acquire the same lock on the owning
+ * table.
+ */
+struct AttachIndexCallbackState
+{
+	Oid		partitionOid;
+	Oid		parentTblOid;
+	bool	lockedParentTbl;
+};
+
+static void
+RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid,
+							   void *arg)
+{
+	struct AttachIndexCallbackState *state;
+	Form_pg_class classform;
+	HeapTuple	tuple;
+
+	state = (struct AttachIndexCallbackState *) arg;
+
+	if (!state->lockedParentTbl)
+	{
+		LockRelationOid(state->parentTblOid, AccessShareLock);
+		state->lockedParentTbl = true;
+	}
+
+	/*
+	 * If we previously locked some other heap, and the name we're looking up
+	 * no longer refers to an index on that relation, release the now-useless
+	 * lock.  XXX maybe we should do *after* we verify whether the index does
+	 * not actually belong to the same relation ...
+	 */
+	if (relOid != oldRelOid && OidIsValid(state->partitionOid))
+	{
+		UnlockRelationOid(state->partitionOid, AccessShareLock);
+		state->partitionOid = InvalidOid;
+	}
+
+	/* Didn't find a relation, so no need for locking or permission checks. */
+	if (!OidIsValid(relOid))
+		return;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+	if (!HeapTupleIsValid(tuple))
+		return;					/* concurrently dropped, so nothing to do */
+	classform = (Form_pg_class) GETSTRUCT(tuple);
+	if (classform->relkind != RELKIND_PARTITIONED_INDEX &&
+		classform->relkind != RELKIND_INDEX)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("\"%s\" is not an index", rv->relname)));
+	ReleaseSysCache(tuple);
+
+	/*
+	 * Since we need only examine the heap's tupledesc, an access share lock
+	 * on it (preventing any DDL) is sufficient.
+	 */
+	state->partitionOid = IndexGetRelation(relOid, false);
+	LockRelationOid(state->partitionOid, AccessShareLock);
+}
+
+/*
+ * ALTER INDEX i1 ATTACH PARTITION i2
+ */
+static ObjectAddress
+ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
+{
+	Relation	partIdx;
+	Relation	partTbl;
+	Relation	parentTbl;
+	ObjectAddress address;
+	Oid			partIdxId;
+	Oid			currParent;
+	struct AttachIndexCallbackState state;
+
+	/*
+	 * We need to obtain lock on the index 'name' to modify it, but we also
+	 * need to read its owning table's tuple descriptor -- so we need to lock
+	 * both.  To avoid deadlocks, obtain lock on the table before doing so on
+	 * the index.  Furthermore, we need to examine the parent table of the
+	 * partition, so lock that one too.
+	 */
+	state.partitionOid = InvalidOid;
+	state.parentTblOid = parentIdx->rd_index->indrelid;
+	state.lockedParentTbl = false;
+	partIdxId =
+		RangeVarGetRelidExtended(name, AccessExclusiveLock, false, false,
+								 RangeVarCallbackForAttachIndex,
+								 (void *) &state);
+	/* Not there? */
+	if (!OidIsValid(partIdxId))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("index \"%s\" does not exist", name->relname)));
+
+	/* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */
+	partIdx = relation_open(partIdxId, AccessExclusiveLock);
+
+	/* we already hold locks on both tables, so this is safe: */
+	parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock);
+	partTbl = relation_open(partIdx->rd_index->indrelid, NoLock);
+
+	ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx));
+
+	/* Silently do nothing if already in the right state */
+	currParent = !has_superclass(partIdxId) ? InvalidOid :
+		get_partition_parent(partIdxId);
+	if (currParent != RelationGetRelid(parentIdx))
+	{
+		IndexInfo  *childInfo;
+		IndexInfo  *parentInfo;
+		AttrNumber *attmap;
+		bool		found;
+		int			i;
+		PartitionDesc partDesc;
+
+		/*
+		 * If this partition already has an index attached, refuse the operation.
+		 */
+		refuseDupeIndexAttach(parentIdx, partIdx, partTbl);
+
+		if (OidIsValid(currParent))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+							RelationGetRelationName(partIdx),
+							RelationGetRelationName(parentIdx)),
+					 errdetail("Index \"%s\" is already attached to another index.",
+							   RelationGetRelationName(partIdx))));
+
+		/* Make sure it indexes a partition of the other index's table */
+		partDesc = RelationGetPartitionDesc(parentTbl);
+		found = false;
+		for (i = 0; i < partDesc->nparts; i++)
+		{
+			if (partDesc->oids[i] == state.partitionOid)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			ereport(ERROR,
+					(errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+							RelationGetRelationName(partIdx),
+							RelationGetRelationName(parentIdx)),
+					 errdetail("Index \"%s\" is not an index on any partition of table \"%s\".",
+							   RelationGetRelationName(partIdx),
+							   RelationGetRelationName(parentTbl))));
+
+		/* Ensure the indexes are compatible */
+		childInfo = BuildIndexInfo(partIdx);
+		parentInfo = BuildIndexInfo(parentIdx);
+		attmap = convert_tuples_by_name_map(RelationGetDescr(partTbl),
+											RelationGetDescr(parentTbl),
+											gettext_noop("could not convert row type"));
+		if (!CompareIndexInfo(childInfo, parentInfo,
+							  partIdx->rd_indcollation,
+							  parentIdx->rd_indcollation,
+							  partIdx->rd_opfamily,
+							  parentIdx->rd_opfamily,
+							  attmap,
+							  RelationGetDescr(partTbl)->natts))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+							RelationGetRelationName(partIdx),
+							RelationGetRelationName(parentIdx)),
+					 errdetail("The index definitions do not match.")));
+
+		/* All good -- do it */
+		IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx));
+		pfree(attmap);
+
+		CommandCounterIncrement();
+
+		validatePartitionedIndex(parentIdx, parentTbl);
+	}
+
+	relation_close(parentTbl, AccessShareLock);
+	/* keep these locks till commit */
+	relation_close(partTbl, NoLock);
+	relation_close(partIdx, NoLock);
+
+	return address;
+}
+
+/*
+ * Verify whether the given partition already contains an index attached
+ * to the given partitioned index.  If so, raise an error.
+ */
+static void
+refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl)
+{
+	Relation		pg_inherits;
+	ScanKeyData		key;
+	HeapTuple		tuple;
+	SysScanDesc		scan;
+
+	pg_inherits = heap_open(InheritsRelationId, AccessShareLock);
+	ScanKeyInit(&key, Anum_pg_inherits_inhparent,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(parentIdx)));
+	scan = systable_beginscan(pg_inherits, InheritsParentIndexId, true,
+							  NULL, 1, &key);
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_inherits	inhForm;
+		Oid			tab;
+
+		inhForm = (Form_pg_inherits) GETSTRUCT(tuple);
+		tab = IndexGetRelation(inhForm->inhrelid, false);
+		if (tab == RelationGetRelid(partitionTbl))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+							RelationGetRelationName(partIdx),
+							RelationGetRelationName(parentIdx)),
+					 errdetail("Another index is already attached for partition \"%s\".",
+							   RelationGetRelationName(partitionTbl))));
+	}
+
+	systable_endscan(scan);
+	heap_close(pg_inherits, AccessShareLock);
+}
+
+/*
+ * Verify whether the set of attached partition indexes to a parent index on
+ * a partitioned table is complete.  If it is, mark the parent index valid.
+ *
+ * This should be called each time a partition index is attached.
+ */
+static void
+validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
+{
+	Relation		inheritsRel;
+	SysScanDesc		scan;
+	ScanKeyData		key;
+	int				tuples = 0;
+	HeapTuple		inhTup;
+	bool			updated = false;
+
+	Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX);
+
+	/*
+	 * Scan pg_inherits for this parent index.  Count each valid index we find
+	 * (verifying the pg_index entry for each), and if we reach the total
+	 * amount we expect, we can mark this parent index as valid.
+	 */
+	inheritsRel = heap_open(InheritsRelationId, AccessShareLock);
+	ScanKeyInit(&key, Anum_pg_inherits_inhparent,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(partedIdx)));
+	scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true,
+							  NULL, 1, &key);
+	while ((inhTup = systable_getnext(scan)) != NULL)
+	{
+		Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup);
+		HeapTuple		indTup;
+		Form_pg_index	indexForm;
+
+		indTup = SearchSysCache1(INDEXRELID,
+								ObjectIdGetDatum(inhForm->inhrelid));
+		if (!indTup)
+			elog(ERROR, "cache lookup failed for index %u",
+				 inhForm->inhrelid);
+		indexForm = (Form_pg_index) GETSTRUCT(indTup);
+		if (IndexIsValid(indexForm))
+			tuples += 1;
+		ReleaseSysCache(indTup);
+	}
+
+	/* Done with pg_inherits */
+	systable_endscan(scan);
+	heap_close(inheritsRel, AccessShareLock);
+
+	/*
+	 * If we found as many inherited indexes as the partitioned table has
+	 * partitions, we're good; update pg_index to set indisvalid.
+	 */
+	if (tuples == RelationGetPartitionDesc(partedTbl)->nparts)
+	{
+		Relation	idxRel;
+		HeapTuple	newtup;
+
+		idxRel = heap_open(IndexRelationId, RowExclusiveLock);
+
+		newtup = heap_copytuple(partedIdx->rd_indextuple);
+		((Form_pg_index) GETSTRUCT(newtup))->indisvalid = true;
+		updated = true;
+
+		CatalogTupleUpdate(idxRel, &partedIdx->rd_indextuple->t_self, newtup);
+
+		heap_close(idxRel, RowExclusiveLock);
+	}
+
+	/*
+	 * If this index is in turn a partition of a larger index, validating it
+	 * might cause the parent to become valid also.  Try that.
+	 */
+	if (updated &&
+		has_superclass(RelationGetRelid(partedIdx)))
+	{
+		Oid			parentIdxId,
+					parentTblId;
+		Relation	parentIdx,
+					parentTbl;
+
+		/* make sure we see the validation we just did */
+		CommandCounterIncrement();
+
+		parentIdxId = get_partition_parent(RelationGetRelid(partedIdx));
+		parentTblId = get_partition_parent(RelationGetRelid(partedTbl));
+		parentIdx = relation_open(parentIdxId, AccessExclusiveLock);
+		parentTbl = relation_open(parentTblId, AccessExclusiveLock);
+		Assert(!parentIdx->rd_index->indisvalid);
+
+		validatePartitionedIndex(parentIdx, parentTbl);
+
+		relation_close(parentIdx, AccessExclusiveLock);
+		relation_close(parentTbl, AccessExclusiveLock);
+	}
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ddbbc79823e7721608fcaf73d193fa8775e0d1cf..65d8c77d7aa83fa98f976e48871ac9e5c71c560e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3379,6 +3379,7 @@ _copyIndexStmt(const IndexStmt *from)
 
 	COPY_STRING_FIELD(idxname);
 	COPY_NODE_FIELD(relation);
+	COPY_SCALAR_FIELD(relationId);
 	COPY_STRING_FIELD(accessMethod);
 	COPY_STRING_FIELD(tableSpace);
 	COPY_NODE_FIELD(indexParams);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 30ccc9c5ae7ff737adf6c3496a28f2c300dff547..0bd12e862e508baf8d264e23aa7592ac56dfd668 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1332,6 +1332,7 @@ _equalIndexStmt(const IndexStmt *a, const IndexStmt *b)
 {
 	COMPARE_STRING_FIELD(idxname);
 	COMPARE_NODE_FIELD(relation);
+	COMPARE_SCALAR_FIELD(relationId);
 	COMPARE_STRING_FIELD(accessMethod);
 	COMPARE_STRING_FIELD(tableSpace);
 	COMPARE_NODE_FIELD(indexParams);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 5e72df137e7931d6f4235b0ab59212c40c44e823..b1cdfc36a628fd21c5e6982b16def76e14d7ebc0 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2650,6 +2650,7 @@ _outIndexStmt(StringInfo str, const IndexStmt *node)
 
 	WRITE_STRING_FIELD(idxname);
 	WRITE_NODE_FIELD(relation);
+	WRITE_OID_FIELD(relationId);
 	WRITE_STRING_FIELD(accessMethod);
 	WRITE_STRING_FIELD(tableSpace);
 	WRITE_NODE_FIELD(indexParams);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e42b7caff6b055fd0a44ac685d87b314d67c84bc..93e67e8adcce0adc62c871307beb042d972b707c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -290,7 +290,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	add_drop opt_asc_desc opt_nulls_order
 
 %type <node>	alter_table_cmd alter_type_cmd opt_collate_clause
-	   replica_identity partition_cmd
+	   replica_identity partition_cmd index_partition_cmd
 %type <list>	alter_table_cmds alter_type_cmds
 %type <list>    alter_identity_column_option_list
 %type <defelt>  alter_identity_column_option
@@ -1891,6 +1891,15 @@ AlterTableStmt:
 					n->missing_ok = true;
 					$$ = (Node *)n;
 				}
+		|	ALTER INDEX qualified_name index_partition_cmd
+				{
+					AlterTableStmt *n = makeNode(AlterTableStmt);
+					n->relation = $3;
+					n->cmds = list_make1($4);
+					n->relkind = OBJECT_INDEX;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 		|	ALTER INDEX ALL IN_P TABLESPACE name SET TABLESPACE name opt_nowait
 				{
 					AlterTableMoveAllStmt *n =
@@ -2025,6 +2034,22 @@ partition_cmd:
 				}
 		;
 
+index_partition_cmd:
+			/* ALTER INDEX <name> ATTACH PARTITION <index_name> */
+			ATTACH PARTITION qualified_name
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					PartitionCmd *cmd = makeNode(PartitionCmd);
+
+					n->subtype = AT_AttachPartition;
+					cmd->name = $3;
+					cmd->bound = NULL;
+					n->def = (Node *) cmd;
+
+					$$ = (Node *) n;
+				}
+		;
+
 alter_table_cmd:
 			/* ALTER TABLE <name> ADD <coldef> */
 			ADD_P columnDef
@@ -7330,7 +7355,7 @@ defacl_privilege_target:
  *****************************************************************************/
 
 IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
-			ON qualified_name access_method_clause '(' index_params ')'
+			ON relation_expr access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
@@ -7338,6 +7363,7 @@ IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
 					n->concurrent = $4;
 					n->idxname = $5;
 					n->relation = $7;
+					n->relationId = InvalidOid;
 					n->accessMethod = $8;
 					n->indexParams = $10;
 					n->options = $12;
@@ -7356,7 +7382,7 @@ IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
 					$$ = (Node *)n;
 				}
 			| CREATE opt_unique INDEX opt_concurrently IF_P NOT EXISTS index_name
-			ON qualified_name access_method_clause '(' index_params ')'
+			ON relation_expr access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
@@ -7364,6 +7390,7 @@ IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
 					n->concurrent = $4;
 					n->idxname = $8;
 					n->relation = $10;
+					n->relationId = InvalidOid;
 					n->accessMethod = $11;
 					n->indexParams = $13;
 					n->options = $15;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 128f1679c6be999872a4a81c270bd2d5b89cb53d..90bb356df857334ed93c692e4fa7dd60acf1cda1 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -118,9 +118,6 @@ static void transformTableLikeClause(CreateStmtContext *cxt,
 						 TableLikeClause *table_like_clause);
 static void transformOfType(CreateStmtContext *cxt,
 				TypeName *ofTypename);
-static IndexStmt *generateClonedIndexStmt(CreateStmtContext *cxt,
-						Relation source_idx,
-						const AttrNumber *attmap, int attmap_length);
 static List *get_collation(Oid collation, Oid actual_datatype);
 static List *get_opclass(Oid opclass, Oid actual_datatype);
 static void transformIndexConstraints(CreateStmtContext *cxt);
@@ -1185,7 +1182,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 			parent_index = index_open(parent_index_oid, AccessShareLock);
 
 			/* Build CREATE INDEX statement to recreate the parent_index */
-			index_stmt = generateClonedIndexStmt(cxt, parent_index,
+			index_stmt = generateClonedIndexStmt(cxt->relation, InvalidOid,
+												 parent_index,
 												 attmap, tupleDesc->natts);
 
 			/* Copy comment on index, if requested */
@@ -1263,10 +1261,12 @@ transformOfType(CreateStmtContext *cxt, TypeName *ofTypename)
 
 /*
  * Generate an IndexStmt node using information from an already existing index
- * "source_idx".  Attribute numbers should be adjusted according to attmap.
+ * "source_idx", for the rel identified either by heapRel or heapRelid.
+ *
+ * Attribute numbers should be adjusted according to attmap.
  */
-static IndexStmt *
-generateClonedIndexStmt(CreateStmtContext *cxt, Relation source_idx,
+IndexStmt *
+generateClonedIndexStmt(RangeVar *heapRel, Oid heapRelid, Relation source_idx,
 						const AttrNumber *attmap, int attmap_length)
 {
 	Oid			source_relid = RelationGetRelid(source_idx);
@@ -1287,6 +1287,9 @@ generateClonedIndexStmt(CreateStmtContext *cxt, Relation source_idx,
 	Datum		datum;
 	bool		isnull;
 
+	Assert((heapRel == NULL && OidIsValid(heapRelid)) ||
+		   (heapRel != NULL && !OidIsValid(heapRelid)));
+
 	/*
 	 * Fetch pg_class tuple of source index.  We can't use the copy in the
 	 * relcache entry because it doesn't include optional fields.
@@ -1322,7 +1325,8 @@ generateClonedIndexStmt(CreateStmtContext *cxt, Relation source_idx,
 
 	/* Begin building the IndexStmt */
 	index = makeNode(IndexStmt);
-	index->relation = cxt->relation;
+	index->relation = heapRel;
+	index->relationId = heapRelid;
 	index->accessMethod = pstrdup(NameStr(amrec->amname));
 	if (OidIsValid(idxrelrec->reltablespace))
 		index->tableSpace = get_tablespace_name(idxrelrec->reltablespace);
@@ -3289,18 +3293,39 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 {
 	Relation	parentRel = cxt->rel;
 
-	/* the table must be partitioned */
-	if (parentRel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("\"%s\" is not partitioned",
-						RelationGetRelationName(parentRel))));
-
-	/* transform the partition bound, if any */
-	Assert(RelationGetPartitionKey(parentRel) != NULL);
-	if (cmd->bound != NULL)
-		cxt->partbound = transformPartitionBound(cxt->pstate, parentRel,
-												 cmd->bound);
+	switch (parentRel->rd_rel->relkind)
+	{
+		case RELKIND_PARTITIONED_TABLE:
+			/* transform the partition bound, if any */
+			Assert(RelationGetPartitionKey(parentRel) != NULL);
+			if (cmd->bound != NULL)
+				cxt->partbound = transformPartitionBound(cxt->pstate, parentRel,
+														 cmd->bound);
+			break;
+		case RELKIND_PARTITIONED_INDEX:
+			/* nothing to check */
+			Assert(cmd->bound == NULL);
+			break;
+		case RELKIND_RELATION:
+			/* the table must be partitioned */
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("table \"%s\" is not partitioned",
+							RelationGetRelationName(parentRel))));
+			break;
+		case RELKIND_INDEX:
+			/* the index must be partitioned */
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("index \"%s\" is not partitioned",
+							RelationGetRelationName(parentRel))));
+			break;
+		default:
+			/* parser shouldn't let this case through */
+			elog(ERROR, "\"%s\" is not a partitioned table or index",
+				 RelationGetRelationName(parentRel));
+			break;
+	}
 }
 
 /*
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ec98a612ec9ff34da1ec6648f444654b17150b0e..9cccc8d39de13906a764381c0cba4326092866cb 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -23,6 +23,7 @@
 #include "access/xlog.h"
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_inherits_fn.h"
 #include "catalog/toasting.h"
 #include "commands/alter.h"
 #include "commands/async.h"
@@ -1300,6 +1301,7 @@ ProcessUtilitySlow(ParseState *pstate,
 					IndexStmt  *stmt = (IndexStmt *) parsetree;
 					Oid			relid;
 					LOCKMODE	lockmode;
+					List	   *inheritors = NIL;
 
 					if (stmt->concurrent)
 						PreventTransactionChain(isTopLevel,
@@ -1322,6 +1324,23 @@ ProcessUtilitySlow(ParseState *pstate,
 												 RangeVarCallbackOwnsRelation,
 												 NULL);
 
+					/*
+					 * CREATE INDEX on partitioned tables (but not regular
+					 * inherited tables) recurses to partitions, so we must
+					 * acquire locks early to avoid deadlocks.
+					 */
+					if (stmt->relation->inh)
+					{
+						Relation	rel;
+
+						/* already locked by RangeVarGetRelidExtended */
+						rel = heap_open(relid, NoLock);
+						if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+							inheritors = find_all_inheritors(relid, lockmode,
+															 NULL);
+						heap_close(rel, NoLock);
+					}
+
 					/* Run parse analysis ... */
 					stmt = transformIndexStmt(relid, stmt, queryString);
 
@@ -1331,6 +1350,7 @@ ProcessUtilitySlow(ParseState *pstate,
 						DefineIndex(relid,	/* OID of heap relation */
 									stmt,
 									InvalidOid, /* no predefined OID */
+									InvalidOid, /* no parent index */
 									false,	/* is_alter_table */
 									true,	/* check_rights */
 									true,	/* check_not_in_use */
@@ -1346,6 +1366,8 @@ ProcessUtilitySlow(ParseState *pstate,
 													 parsetree);
 					commandCollected = true;
 					EventTriggerAlterTableEnd();
+
+					list_free(inheritors);
 				}
 				break;
 
diff --git a/src/backend/utils/adt/amutils.c b/src/backend/utils/adt/amutils.c
index a6d8feea5b4800e82b5dfdab1cd2e952f3a5549b..0f7ceb62eb56458645282c30bfa23f8540227c8f 100644
--- a/src/backend/utils/adt/amutils.c
+++ b/src/backend/utils/adt/amutils.c
@@ -183,7 +183,8 @@ indexam_property(FunctionCallInfo fcinfo,
 		if (!HeapTupleIsValid(tuple))
 			PG_RETURN_NULL();
 		rd_rel = (Form_pg_class) GETSTRUCT(tuple);
-		if (rd_rel->relkind != RELKIND_INDEX)
+		if (rd_rel->relkind != RELKIND_INDEX &&
+			rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
 		{
 			ReleaseSysCache(tuple);
 			PG_RETURN_NULL();
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9cdbb06add77a183bf78bffd69f684ed88359144..c5f5a1ca3f92de14374dc06ea2d4ef6511f0bbe1 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -317,7 +317,7 @@ static void decompile_column_index_array(Datum column_index_array, Oid relId,
 static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
 static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
 					   const Oid *excludeOps,
-					   bool attrsOnly, bool showTblSpc,
+					   bool attrsOnly, bool showTblSpc, bool inherits,
 					   int prettyFlags, bool missing_ok);
 static char *pg_get_statisticsobj_worker(Oid statextid, bool missing_ok);
 static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
@@ -1086,7 +1086,7 @@ pg_get_indexdef(PG_FUNCTION_ARGS)
 
 	prettyFlags = PRETTYFLAG_INDENT;
 
-	res = pg_get_indexdef_worker(indexrelid, 0, NULL, false, false,
+	res = pg_get_indexdef_worker(indexrelid, 0, NULL, false, false, false,
 								 prettyFlags, true);
 
 	if (res == NULL)
@@ -1107,7 +1107,7 @@ pg_get_indexdef_ext(PG_FUNCTION_ARGS)
 	prettyFlags = pretty ? PRETTYFLAG_PAREN | PRETTYFLAG_INDENT : PRETTYFLAG_INDENT;
 
 	res = pg_get_indexdef_worker(indexrelid, colno, NULL, colno != 0, false,
-								 prettyFlags, true);
+								 false, prettyFlags, true);
 
 	if (res == NULL)
 		PG_RETURN_NULL();
@@ -1123,7 +1123,7 @@ pg_get_indexdef_ext(PG_FUNCTION_ARGS)
 char *
 pg_get_indexdef_string(Oid indexrelid)
 {
-	return pg_get_indexdef_worker(indexrelid, 0, NULL, false, true, 0, false);
+	return pg_get_indexdef_worker(indexrelid, 0, NULL, false, true, true, 0, false);
 }
 
 /* Internal version that just reports the column definitions */
@@ -1133,7 +1133,7 @@ pg_get_indexdef_columns(Oid indexrelid, bool pretty)
 	int			prettyFlags;
 
 	prettyFlags = pretty ? PRETTYFLAG_PAREN | PRETTYFLAG_INDENT : PRETTYFLAG_INDENT;
-	return pg_get_indexdef_worker(indexrelid, 0, NULL, true, false,
+	return pg_get_indexdef_worker(indexrelid, 0, NULL, true, false, false,
 								  prettyFlags, false);
 }
 
@@ -1146,7 +1146,7 @@ pg_get_indexdef_columns(Oid indexrelid, bool pretty)
 static char *
 pg_get_indexdef_worker(Oid indexrelid, int colno,
 					   const Oid *excludeOps,
-					   bool attrsOnly, bool showTblSpc,
+					   bool attrsOnly, bool showTblSpc, bool inherits,
 					   int prettyFlags, bool missing_ok)
 {
 	/* might want a separate isConstraint parameter later */
@@ -1259,9 +1259,11 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 	if (!attrsOnly)
 	{
 		if (!isConstraint)
-			appendStringInfo(&buf, "CREATE %sINDEX %s ON %s USING %s (",
+			appendStringInfo(&buf, "CREATE %sINDEX %s ON %s%s USING %s (",
 							 idxrec->indisunique ? "UNIQUE " : "",
 							 quote_identifier(NameStr(idxrelrec->relname)),
+							 idxrelrec->relkind == RELKIND_PARTITIONED_INDEX
+							 && !inherits ? "ONLY " : "",
 							 generate_relation_name(indrelid, NIL),
 							 quote_identifier(NameStr(amrec->amname)));
 		else					/* currently, must be EXCLUDE constraint */
@@ -2148,6 +2150,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 															  operators,
 															  false,
 															  false,
+															  false,
 															  prettyFlags,
 															  false));
 				break;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 00ba33bfb44ae8a97afacb667d9d9d15c715dc69..c081b88b7334d5ada81a7a271e1c463234d308ab 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -430,18 +430,26 @@ static void
 RelationParseRelOptions(Relation relation, HeapTuple tuple)
 {
 	bytea	   *options;
+	amoptions_function amoptsfn;
 
 	relation->rd_options = NULL;
 
-	/* Fall out if relkind should not have options */
+	/*
+	 * Look up any AM-specific parse function; fall out if relkind should not
+	 * have options.
+	 */
 	switch (relation->rd_rel->relkind)
 	{
 		case RELKIND_RELATION:
 		case RELKIND_TOASTVALUE:
-		case RELKIND_INDEX:
 		case RELKIND_VIEW:
 		case RELKIND_MATVIEW:
 		case RELKIND_PARTITIONED_TABLE:
+			amoptsfn = NULL;
+			break;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			amoptsfn = relation->rd_amroutine->amoptions;
 			break;
 		default:
 			return;
@@ -452,10 +460,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
 	 * we might not have any other for pg_class yet (consider executing this
 	 * code for pg_class itself)
 	 */
-	options = extractRelOptions(tuple,
-								GetPgClassDescriptor(),
-								relation->rd_rel->relkind == RELKIND_INDEX ?
-								relation->rd_amroutine->amoptions : NULL);
+	options = extractRelOptions(tuple, GetPgClassDescriptor(), amoptsfn);
 
 	/*
 	 * Copy parsed data into CacheMemoryContext.  To guard against the
@@ -2053,7 +2058,8 @@ RelationIdGetRelation(Oid relationId)
 			 * and we don't want to use the full-blown procedure because it's
 			 * a headache for indexes that reload itself depends on.
 			 */
-			if (rd->rd_rel->relkind == RELKIND_INDEX)
+			if (rd->rd_rel->relkind == RELKIND_INDEX ||
+				rd->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 				RelationReloadIndexInfo(rd);
 			else
 				RelationClearRelation(rd, true);
@@ -2167,7 +2173,8 @@ RelationReloadIndexInfo(Relation relation)
 	Form_pg_class relp;
 
 	/* Should be called only for invalidated indexes */
-	Assert(relation->rd_rel->relkind == RELKIND_INDEX &&
+	Assert((relation->rd_rel->relkind == RELKIND_INDEX ||
+			relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX) &&
 		   !relation->rd_isvalid);
 
 	/* Ensure it's closed at smgr level */
@@ -2387,7 +2394,8 @@ RelationClearRelation(Relation relation, bool rebuild)
 	{
 		RelationInitPhysicalAddr(relation);
 
-		if (relation->rd_rel->relkind == RELKIND_INDEX)
+		if (relation->rd_rel->relkind == RELKIND_INDEX ||
+			relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 		{
 			relation->rd_isvalid = false;	/* needs to be revalidated */
 			if (relation->rd_refcnt > 1 && IsTransactionState())
@@ -2403,7 +2411,8 @@ RelationClearRelation(Relation relation, bool rebuild)
 	 * re-read the pg_class row to handle possible physical relocation of the
 	 * index, and we check for pg_index updates too.
 	 */
-	if (relation->rd_rel->relkind == RELKIND_INDEX &&
+	if ((relation->rd_rel->relkind == RELKIND_INDEX ||
+		 relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX) &&
 		relation->rd_refcnt > 0 &&
 		relation->rd_indexcxt != NULL)
 	{
@@ -5461,7 +5470,10 @@ load_relcache_init_file(bool shared)
 			rel->rd_att->constr = constr;
 		}
 
-		/* If it's an index, there's more to do */
+		/*
+		 * If it's an index, there's more to do.  Note we explicitly ignore
+		 * partitioned indexes here.
+		 */
 		if (rel->rd_rel->relkind == RELKIND_INDEX)
 		{
 			MemoryContext indexcxt;
@@ -5825,7 +5837,10 @@ write_relcache_init_file(bool shared)
 				   (rel->rd_options ? VARSIZE(rel->rd_options) : 0),
 				   fp);
 
-		/* If it's an index, there's more to do */
+		/*
+		 * If it's an index, there's more to do. Note we explicitly ignore
+		 * partitioned indexes here.
+		 */
 		if (rel->rd_rel->relkind == RELKIND_INDEX)
 		{
 			/* write the pg_index tuple */
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 7f5f35148682058168a38f8fe632af7a3ba00d92..2ec3627a68e14031803d678c48654df31ca40457 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -68,6 +68,7 @@ static int	numextmembers;
 
 static void flagInhTables(Archive *fout, TableInfo *tbinfo, int numTables,
 			  InhInfo *inhinfo, int numInherits);
+static void flagInhIndexes(Archive *fout, TableInfo *tblinfo, int numTables);
 static void flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables);
 static DumpableObject **buildIndexArray(void *objArray, int numObjs,
 				Size objSize);
@@ -76,6 +77,8 @@ static int	ExtensionMemberIdCompare(const void *p1, const void *p2);
 static void findParentsByOid(TableInfo *self,
 				 InhInfo *inhinfo, int numInherits);
 static int	strInArray(const char *pattern, char **arr, int arr_size);
+static IndxInfo *findIndexByOid(Oid oid, DumpableObject **idxinfoindex,
+			   int numIndexes);
 
 
 /*
@@ -257,6 +260,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 		write_msg(NULL, "reading indexes\n");
 	getIndexes(fout, tblinfo, numTables);
 
+	if (g_verbose)
+		write_msg(NULL, "flagging indexes in partitioned tables\n");
+	flagInhIndexes(fout, tblinfo, numTables);
+
 	if (g_verbose)
 		write_msg(NULL, "reading extended statistics\n");
 	getExtendedStatistics(fout, tblinfo, numTables);
@@ -342,7 +349,10 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables,
 		if (find_parents)
 			findParentsByOid(&tblinfo[i], inhinfo, numInherits);
 
-		/* If needed, mark the parents as interesting for getTableAttrs. */
+		/*
+		 * If needed, mark the parents as interesting for getTableAttrs
+		 * and getIndexes.
+		 */
 		if (mark_parents)
 		{
 			int			numParents = tblinfo[i].numParents;
@@ -354,6 +364,89 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables,
 	}
 }
 
+/*
+ * flagInhIndexes -
+ *	 Create AttachIndexInfo objects for partitioned indexes, and add
+ *	 appropriate dependency links.
+ */
+static void
+flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+	int		i,
+			j,
+			k;
+	DumpableObject ***parentIndexArray;
+
+	parentIndexArray = (DumpableObject ***)
+		pg_malloc0(getMaxDumpId() * sizeof(DumpableObject **));
+
+	for (i = 0; i < numTables; i++)
+	{
+		TableInfo	   *parenttbl;
+		IndexAttachInfo *attachinfo;
+
+		if (!tblinfo[i].ispartition || tblinfo[i].numParents == 0)
+			continue;
+
+		Assert(tblinfo[i].numParents == 1);
+		parenttbl = tblinfo[i].parents[0];
+
+		/*
+		 * We need access to each parent table's index list, but there is no
+		 * index to cover them outside of this function.  To avoid having to
+		 * sort every parent table's indexes each time we come across each of
+		 * its partitions, create an indexed array for each parent the first
+		 * time it is required.
+		 */
+		if (parentIndexArray[parenttbl->dobj.dumpId] == NULL)
+			parentIndexArray[parenttbl->dobj.dumpId] =
+				buildIndexArray(parenttbl->indexes,
+								parenttbl->numIndexes,
+								sizeof(IndxInfo));
+
+		attachinfo = (IndexAttachInfo *)
+			pg_malloc0(tblinfo[i].numIndexes * sizeof(IndexAttachInfo));
+		for (j = 0, k = 0; j < tblinfo[i].numIndexes; j++)
+		{
+			IndxInfo   *index = &(tblinfo[i].indexes[j]);
+			IndxInfo   *parentidx;
+
+			if (index->parentidx == 0)
+				continue;
+
+			parentidx = findIndexByOid(index->parentidx,
+									   parentIndexArray[parenttbl->dobj.dumpId],
+									   parenttbl->numIndexes);
+			if (parentidx == NULL)
+				continue;
+
+			attachinfo[k].dobj.objType = DO_INDEX_ATTACH;
+			attachinfo[k].dobj.catId.tableoid = 0;
+			attachinfo[k].dobj.catId.oid = 0;
+			AssignDumpId(&attachinfo[k].dobj);
+			attachinfo[k].dobj.name = pg_strdup(index->dobj.name);
+			attachinfo[k].parentIdx = parentidx;
+			attachinfo[k].partitionIdx = index;
+
+			/*
+			 * We want dependencies from parent to partition (so that the
+			 * partition index is created first), and another one from
+			 * attach object to parent (so that the partition index is
+			 * attached once the parent index has been created).
+			 */
+			addObjectDependency(&parentidx->dobj, index->dobj.dumpId);
+			addObjectDependency(&attachinfo[k].dobj, parentidx->dobj.dumpId);
+
+			k++;
+		}
+	}
+
+	for (i = 0; i < numTables; i++)
+		if (parentIndexArray[i])
+			pg_free(parentIndexArray[i]);
+	pg_free(parentIndexArray);
+}
+
 /* flagInhAttrs -
  *	 for each dumpable table in tblinfo, flag its inherited attributes
  *
@@ -827,6 +920,18 @@ findExtensionByOid(Oid oid)
 	return (ExtensionInfo *) findObjectByOid(oid, extinfoindex, numExtensions);
 }
 
+/*
+ * findIndexByOid
+ *		find the entry of the index with the given oid
+ *
+ * This one's signature is different from the previous ones because we lack a
+ * global array of all indexes, so caller must pass their array as argument.
+ */
+static IndxInfo *
+findIndexByOid(Oid oid, DumpableObject **idxinfoindex, int numIndexes)
+{
+	return (IndxInfo *) findObjectByOid(oid, idxinfoindex, numIndexes);
+}
 
 /*
  * setExtensionMembership
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 27628a397cdd5d7dcfb1ad215b011a2c5a0ce24d..af2d03ed19d145689d7096caab6e3a44f5e537e6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -193,6 +193,7 @@ static void dumpAttrDef(Archive *fout, AttrDefInfo *adinfo);
 static void dumpSequence(Archive *fout, TableInfo *tbinfo);
 static void dumpSequenceData(Archive *fout, TableDataInfo *tdinfo);
 static void dumpIndex(Archive *fout, IndxInfo *indxinfo);
+static void dumpIndexAttach(Archive *fout, IndexAttachInfo *attachinfo);
 static void dumpStatisticsExt(Archive *fout, StatsExtInfo *statsextinfo);
 static void dumpConstraint(Archive *fout, ConstraintInfo *coninfo);
 static void dumpTableConstraintComment(Archive *fout, ConstraintInfo *coninfo);
@@ -6509,6 +6510,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	int			i_tableoid,
 				i_oid,
 				i_indexname,
+				i_parentidx,
 				i_indexdef,
 				i_indnkeys,
 				i_indkey,
@@ -6530,15 +6532,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	{
 		TableInfo  *tbinfo = &tblinfo[i];
 
-		/* Only plain tables and materialized views have indexes. */
-		if (tbinfo->relkind != RELKIND_RELATION &&
-			tbinfo->relkind != RELKIND_MATVIEW)
-			continue;
 		if (!tbinfo->hasindex)
 			continue;
 
-		/* Ignore indexes of tables whose definitions are not to be dumped */
-		if (!(tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION))
+		/*
+		 * Ignore indexes of tables whose definitions are not to be dumped.
+		 *
+		 * We also need indexes on partitioned tables which have partitions to
+		 * be dumped, in order to dump the indexes on the partitions.
+		 */
+		if (!(tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) &&
+			!tbinfo->interesting)
 			continue;
 
 		if (g_verbose)
@@ -6561,7 +6565,39 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		 * is not.
 		 */
 		resetPQExpBuffer(query);
-		if (fout->remoteVersion >= 90400)
+		if (fout->remoteVersion >= 11000)
+		{
+			appendPQExpBuffer(query,
+							  "SELECT t.tableoid, t.oid, "
+							  "t.relname AS indexname, "
+							  "inh.inhparent AS parentidx, "
+							  "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
+							  "t.relnatts AS indnkeys, "
+							  "i.indkey, i.indisclustered, "
+							  "i.indisreplident, t.relpages, "
+							  "c.contype, c.conname, "
+							  "c.condeferrable, c.condeferred, "
+							  "c.tableoid AS contableoid, "
+							  "c.oid AS conoid, "
+							  "pg_catalog.pg_get_constraintdef(c.oid, false) AS condef, "
+							  "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
+							  "t.reloptions AS indreloptions "
+							  "FROM pg_catalog.pg_index i "
+							  "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
+							  "JOIN pg_catalog.pg_class t2 ON (t2.oid = i.indrelid) "
+							  "LEFT JOIN pg_catalog.pg_constraint c "
+							  "ON (i.indrelid = c.conrelid AND "
+							  "i.indexrelid = c.conindid AND "
+							  "c.contype IN ('p','u','x')) "
+							  "LEFT JOIN pg_catalog.pg_inherits inh "
+							  "ON (inh.inhrelid = indexrelid) "
+							  "WHERE i.indrelid = '%u'::pg_catalog.oid "
+							  "AND (i.indisvalid OR t2.relkind = 'p') "
+							  "AND i.indisready "
+							  "ORDER BY indexname",
+							  tbinfo->dobj.catId.oid);
+		}
+		else if (fout->remoteVersion >= 90400)
 		{
 			/*
 			 * the test on indisready is necessary in 9.2, and harmless in
@@ -6570,6 +6606,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			appendPQExpBuffer(query,
 							  "SELECT t.tableoid, t.oid, "
 							  "t.relname AS indexname, "
+							  "0 AS parentidx, "
 							  "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 							  "t.relnatts AS indnkeys, "
 							  "i.indkey, i.indisclustered, "
@@ -6601,6 +6638,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			appendPQExpBuffer(query,
 							  "SELECT t.tableoid, t.oid, "
 							  "t.relname AS indexname, "
+							  "0 AS parentidx, "
 							  "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 							  "t.relnatts AS indnkeys, "
 							  "i.indkey, i.indisclustered, "
@@ -6628,6 +6666,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			appendPQExpBuffer(query,
 							  "SELECT t.tableoid, t.oid, "
 							  "t.relname AS indexname, "
+							  "0 AS parentidx, "
 							  "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 							  "t.relnatts AS indnkeys, "
 							  "i.indkey, i.indisclustered, "
@@ -6658,6 +6697,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			appendPQExpBuffer(query,
 							  "SELECT t.tableoid, t.oid, "
 							  "t.relname AS indexname, "
+							  "0 AS parentidx, "
 							  "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 							  "t.relnatts AS indnkeys, "
 							  "i.indkey, i.indisclustered, "
@@ -6690,6 +6730,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		i_tableoid = PQfnumber(res, "tableoid");
 		i_oid = PQfnumber(res, "oid");
 		i_indexname = PQfnumber(res, "indexname");
+		i_parentidx = PQfnumber(res, "parentidx");
 		i_indexdef = PQfnumber(res, "indexdef");
 		i_indnkeys = PQfnumber(res, "indnkeys");
 		i_indkey = PQfnumber(res, "indkey");
@@ -6706,8 +6747,10 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		i_tablespace = PQfnumber(res, "tablespace");
 		i_indreloptions = PQfnumber(res, "indreloptions");
 
-		indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
+		tbinfo->indexes = indxinfo =
+			(IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
 		constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo));
+		tbinfo->numIndexes = ntups;
 
 		for (j = 0; j < ntups; j++)
 		{
@@ -6717,6 +6760,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
 			indxinfo[j].indextable = tbinfo;
@@ -6729,6 +6773,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 						  indxinfo[j].indkeys, indxinfo[j].indnkeys);
 			indxinfo[j].indisclustered = (PQgetvalue(res, j, i_indisclustered)[0] == 't');
 			indxinfo[j].indisreplident = (PQgetvalue(res, j, i_indisreplident)[0] == 't');
+			indxinfo[j].parentidx = atooid(PQgetvalue(res, j, i_parentidx));
 			indxinfo[j].relpages = atoi(PQgetvalue(res, j, i_relpages));
 			contype = *(PQgetvalue(res, j, i_contype));
 
@@ -6742,6 +6787,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_contableoid));
 				constrinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_conoid));
 				AssignDumpId(&constrinfo[j].dobj);
+				constrinfo[j].dobj.dump = tbinfo->dobj.dump;
 				constrinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
 				constrinfo[j].dobj.namespace = tbinfo->dobj.namespace;
 				constrinfo[j].contable = tbinfo;
@@ -9512,6 +9558,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_INDEX:
 			dumpIndex(fout, (IndxInfo *) dobj);
 			break;
+		case DO_INDEX_ATTACH:
+			dumpIndexAttach(fout, (IndexAttachInfo *) dobj);
+			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (StatsExtInfo *) dobj);
 			break;
@@ -16172,6 +16221,42 @@ dumpIndex(Archive *fout, IndxInfo *indxinfo)
 	destroyPQExpBuffer(labelq);
 }
 
+/*
+ * dumpIndexAttach
+ *	  write out to fout a partitioned-index attachment clause
+ */
+void
+dumpIndexAttach(Archive *fout, IndexAttachInfo *attachinfo)
+{
+	if (fout->dopt->dataOnly)
+		return;
+
+	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
+	{
+		PQExpBuffer	q = createPQExpBuffer();
+
+		appendPQExpBuffer(q, "\nALTER INDEX %s ",
+						  fmtQualifiedId(fout->remoteVersion,
+										 attachinfo->parentIdx->dobj.namespace->dobj.name,
+										 attachinfo->parentIdx->dobj.name));
+		appendPQExpBuffer(q, "ATTACH PARTITION %s;\n",
+						  fmtQualifiedId(fout->remoteVersion,
+										 attachinfo->partitionIdx->dobj.namespace->dobj.name,
+										 attachinfo->partitionIdx->dobj.name));
+
+		ArchiveEntry(fout, attachinfo->dobj.catId, attachinfo->dobj.dumpId,
+					 attachinfo->dobj.name,
+					 NULL, NULL,
+					 "",
+					 false, "INDEX ATTACH", SECTION_POST_DATA,
+					 q->data, "", NULL,
+					 NULL, 0,
+					 NULL, NULL);
+
+		destroyPQExpBuffer(q);
+	}
+}
+
 /*
  * dumpStatisticsExt
  *	  write out to fout an extended statistics object
@@ -17803,6 +17888,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				addObjectDependency(postDataBound, dobj->dumpId);
 				break;
 			case DO_INDEX:
+			case DO_INDEX_ATTACH:
 			case DO_STATSEXT:
 			case DO_REFRESH_MATVIEW:
 			case DO_TRIGGER:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 49a02b4fa83f2f2ca91a31a2a15d8235f3afdcf7..6c18d451ef31969b57966e0718d9cc4720e448d7 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -56,6 +56,7 @@ typedef enum
 	DO_TABLE,
 	DO_ATTRDEF,
 	DO_INDEX,
+	DO_INDEX_ATTACH,
 	DO_STATSEXT,
 	DO_RULE,
 	DO_TRIGGER,
@@ -328,6 +329,8 @@ typedef struct _tableInfo
 	 */
 	int			numParents;		/* number of (immediate) parent tables */
 	struct _tableInfo **parents;	/* TableInfos of immediate parents */
+	int			numIndexes;		/* number of indexes */
+	struct _indxInfo *indexes;	/* indexes */
 	struct _tableDataInfo *dataObj; /* TableDataInfo, if dumping its data */
 	int			numTriggers;	/* number of triggers for table */
 	struct _triggerInfo *triggers;	/* array of TriggerInfo structs */
@@ -361,11 +364,19 @@ typedef struct _indxInfo
 	Oid		   *indkeys;
 	bool		indisclustered;
 	bool		indisreplident;
+	Oid			parentidx;		/* if partitioned, parent index OID */
 	/* if there is an associated constraint object, its dumpId: */
 	DumpId		indexconstraint;
 	int			relpages;		/* relpages of the underlying table */
 } IndxInfo;
 
+typedef struct _indexAttachInfo
+{
+	DumpableObject dobj;
+	IndxInfo   *parentIdx;		/* link to index on partitioned table */
+	IndxInfo   *partitionIdx;	/* link to index on partition */
+} IndexAttachInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 6da1c35a42500d577ff8bf8c258c00bf227dd435..5ce3c5d485cc0abf32dc698d34602a6677df3715 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -35,6 +35,10 @@ static const char *modulename = gettext_noop("sorter");
  * pg_dump.c; that is, PRE_DATA objects must sort before DO_PRE_DATA_BOUNDARY,
  * POST_DATA objects must sort after DO_POST_DATA_BOUNDARY, and DATA objects
  * must sort between them.
+ *
+ * Note: sortDataAndIndexObjectsBySize wants to have all DO_TABLE_DATA and
+ * DO_INDEX objects in contiguous chunks, so do not reuse the values for those
+ * for other object types.
  */
 static const int dbObjectTypePriority[] =
 {
@@ -53,11 +57,12 @@ static const int dbObjectTypePriority[] =
 	18,							/* DO_TABLE */
 	20,							/* DO_ATTRDEF */
 	28,							/* DO_INDEX */
-	29,							/* DO_STATSEXT */
-	30,							/* DO_RULE */
-	31,							/* DO_TRIGGER */
+	29,							/* DO_INDEX_ATTACH */
+	30,							/* DO_STATSEXT */
+	31,							/* DO_RULE */
+	32,							/* DO_TRIGGER */
 	27,							/* DO_CONSTRAINT */
-	32,							/* DO_FK_CONSTRAINT */
+	33,							/* DO_FK_CONSTRAINT */
 	2,							/* DO_PROCLANG */
 	10,							/* DO_CAST */
 	23,							/* DO_TABLE_DATA */
@@ -69,18 +74,18 @@ static const int dbObjectTypePriority[] =
 	15,							/* DO_TSCONFIG */
 	16,							/* DO_FDW */
 	17,							/* DO_FOREIGN_SERVER */
-	32,							/* DO_DEFAULT_ACL */
+	33,							/* DO_DEFAULT_ACL */
 	3,							/* DO_TRANSFORM */
 	21,							/* DO_BLOB */
 	25,							/* DO_BLOB_DATA */
 	22,							/* DO_PRE_DATA_BOUNDARY */
 	26,							/* DO_POST_DATA_BOUNDARY */
-	33,							/* DO_EVENT_TRIGGER */
-	38,							/* DO_REFRESH_MATVIEW */
-	34,							/* DO_POLICY */
-	35,							/* DO_PUBLICATION */
-	36,							/* DO_PUBLICATION_REL */
-	37							/* DO_SUBSCRIPTION */
+	34,							/* DO_EVENT_TRIGGER */
+	39,							/* DO_REFRESH_MATVIEW */
+	35,							/* DO_POLICY */
+	36,							/* DO_PUBLICATION */
+	37,							/* DO_PUBLICATION_REL */
+	38							/* DO_SUBSCRIPTION */
 };
 
 static DumpId preDataBoundId;
@@ -937,6 +942,13 @@ repairDomainConstraintMultiLoop(DumpableObject *domainobj,
 	addObjectDependency(constraintobj, postDataBoundId);
 }
 
+static void
+repairIndexLoop(DumpableObject *partedindex,
+				DumpableObject *partindex)
+{
+	removeObjectDependency(partedindex, partindex->dumpId);
+}
+
 /*
  * Fix a dependency loop, or die trying ...
  *
@@ -1099,6 +1111,23 @@ repairDependencyLoop(DumpableObject **loop,
 		return;
 	}
 
+	/* index on partitioned table and corresponding index on partition */
+	if (nLoop == 2 &&
+		loop[0]->objType == DO_INDEX &&
+		loop[1]->objType == DO_INDEX)
+	{
+		if (((IndxInfo *) loop[0])->parentidx == loop[1]->catId.oid)
+		{
+			repairIndexLoop(loop[0], loop[1]);
+			return;
+		}
+		else if (((IndxInfo *) loop[1])->parentidx == loop[0]->catId.oid)
+		{
+			repairIndexLoop(loop[1], loop[0]);
+			return;
+		}
+	}
+
 	/* Indirect loop involving table and attribute default */
 	if (nLoop > 2)
 	{
@@ -1292,6 +1321,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "INDEX %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_INDEX_ATTACH:
+			snprintf(buf, bufsize,
+					 "INDEX ATTACH %s  (ID %d)",
+					 obj->name, obj->dumpId);
+			return;
 		case DO_STATSEXT:
 			snprintf(buf, bufsize,
 					 "STATISTICS %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 7cf9bdadb2e6b9e81a57f9c7cacf12c075cdca0b..fce1465c118e3c939d96494f29537ff9db594426 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -5203,6 +5203,101 @@ qr/CREATE TRANSFORM FOR integer LANGUAGE sql \(FROM SQL WITH FUNCTION pg_catalog
 			section_pre_data         => 1,
 			test_schema_plus_blobs   => 1, }, },
 
+	'CREATE INDEX ON ONLY measurement' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		create_order => 92,
+		create_sql   => 'CREATE INDEX ON dump_test.measurement (city_id, logdate);',
+		regexp => qr/^
+		\QCREATE INDEX measurement_city_id_logdate_idx ON ONLY measurement USING\E
+		/xm,
+		like => {
+			binary_upgrade           => 1,
+			clean                    => 1,
+			clean_if_exists          => 1,
+			createdb                 => 1,
+			defaults                 => 1,
+			exclude_test_table       => 1,
+			exclude_test_table_data  => 1,
+			no_blobs                 => 1,
+			no_privs                 => 1,
+			no_owner                 => 1,
+			only_dump_test_schema    => 1,
+			pg_dumpall_dbprivs       => 1,
+			schema_only              => 1,
+			section_post_data        => 1,
+			test_schema_plus_blobs   => 1,
+			with_oids                => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_pre_data         => 1, }, },
+
+	'CREATE INDEX ... ON measurement_y2006_m2' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		regexp       => qr/^
+		\QCREATE INDEX measurement_y2006m2_city_id_logdate_idx ON measurement_y2006m2 \E
+		/xm,
+		like => {
+			binary_upgrade           => 1,
+			clean                    => 1,
+			clean_if_exists          => 1,
+			createdb                 => 1,
+			defaults                 => 1,
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+			exclude_test_table_data  => 1,
+			no_blobs                 => 1,
+			no_privs                 => 1,
+			no_owner                 => 1,
+			pg_dumpall_dbprivs       => 1,
+			role                     => 1,
+			schema_only              => 1,
+			section_post_data        => 1,
+			with_oids                => 1, },
+		unlike => {
+			only_dump_test_schema    => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			section_pre_data         => 1,
+			test_schema_plus_blobs   => 1, }, },
+
+	'ALTER INDEX ... ATTACH PARTITION' => {
+		all_runs     => 1,
+		catch_all    => 'CREATE ... commands',
+		regexp       => qr/^
+		\QALTER INDEX dump_test.measurement_city_id_logdate_idx ATTACH PARTITION dump_test_second_schema.measurement_y2006m2_city_id_logdate_idx\E
+		/xm,
+		like => {
+			binary_upgrade           => 1,
+			clean                    => 1,
+			clean_if_exists          => 1,
+			createdb                 => 1,
+			defaults                 => 1,
+			exclude_dump_test_schema => 1,
+			exclude_test_table       => 1,
+			exclude_test_table_data  => 1,
+			no_blobs                 => 1,
+			no_privs                 => 1,
+			no_owner                 => 1,
+			pg_dumpall_dbprivs       => 1,
+			role                     => 1,
+			schema_only              => 1,
+			section_post_data        => 1,
+			with_oids                => 1, },
+		unlike => {
+			only_dump_test_schema    => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			section_pre_data         => 1,
+			test_schema_plus_blobs   => 1, }, },
+
 	'CREATE VIEW test_view' => {
 		all_runs     => 1,
 		catch_all    => 'CREATE ... commands',
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index d2787ab41b2e79853b9a116180e9febc96b18b21..466a78004b65069bb15009f3bbcc1579e635c9a7 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1705,7 +1705,8 @@ describeOneTableDetails(const char *schemaname,
 		appendPQExpBufferStr(&buf, ",\n  a.attidentity");
 	else
 		appendPQExpBufferStr(&buf, ",\n  ''::pg_catalog.char AS attidentity");
-	if (tableinfo.relkind == RELKIND_INDEX)
+	if (tableinfo.relkind == RELKIND_INDEX ||
+		tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
 		appendPQExpBufferStr(&buf, ",\n  pg_catalog.pg_get_indexdef(a.attrelid, a.attnum, TRUE) AS indexdef");
 	else
 		appendPQExpBufferStr(&buf, ",\n  NULL AS indexdef");
@@ -1766,6 +1767,7 @@ describeOneTableDetails(const char *schemaname,
 								  schemaname, relationname);
 			break;
 		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
 			if (tableinfo.relpersistence == 'u')
 				printfPQExpBuffer(&title, _("Unlogged index \"%s.%s\""),
 								  schemaname, relationname);
@@ -1823,7 +1825,8 @@ describeOneTableDetails(const char *schemaname,
 		show_column_details = true;
 	}
 
-	if (tableinfo.relkind == RELKIND_INDEX)
+	if (tableinfo.relkind == RELKIND_INDEX ||
+		tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
 		headers[cols++] = gettext_noop("Definition");
 
 	if (tableinfo.relkind == RELKIND_FOREIGN_TABLE && pset.sversion >= 90200)
@@ -1834,6 +1837,7 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
+			tableinfo.relkind == RELKIND_PARTITIONED_INDEX ||
 			tableinfo.relkind == RELKIND_MATVIEW ||
 			tableinfo.relkind == RELKIND_FOREIGN_TABLE ||
 			tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
@@ -1906,7 +1910,8 @@ describeOneTableDetails(const char *schemaname,
 		}
 
 		/* Expression for index column */
-		if (tableinfo.relkind == RELKIND_INDEX)
+		if (tableinfo.relkind == RELKIND_INDEX ||
+			tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
 			printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false);
 
 		/* FDW options for foreign table column, only for 9.2 or later */
@@ -1930,6 +1935,7 @@ describeOneTableDetails(const char *schemaname,
 			/* Statistics target, if the relkind supports this feature */
 			if (tableinfo.relkind == RELKIND_RELATION ||
 				tableinfo.relkind == RELKIND_INDEX ||
+				tableinfo.relkind == RELKIND_PARTITIONED_INDEX ||
 				tableinfo.relkind == RELKIND_MATVIEW ||
 				tableinfo.relkind == RELKIND_FOREIGN_TABLE ||
 				tableinfo.relkind == RELKIND_PARTITIONED_TABLE)
@@ -2021,7 +2027,8 @@ describeOneTableDetails(const char *schemaname,
 		PQclear(result);
 	}
 
-	if (tableinfo.relkind == RELKIND_INDEX)
+	if (tableinfo.relkind == RELKIND_INDEX ||
+		tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
 	{
 		/* Footer information about an index */
 		PGresult   *result;
@@ -3397,6 +3404,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  " WHEN 's' THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'"
+					  " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'"
 					  " END as \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"",
 					  gettext_noop("Schema"),
@@ -3409,6 +3417,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  gettext_noop("special"),
 					  gettext_noop("foreign table"),
 					  gettext_noop("table"),	/* partitioned table */
+					  gettext_noop("index"),	/* partitioned index */
 					  gettext_noop("Type"),
 					  gettext_noop("Owner"));
 
@@ -3454,7 +3463,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	if (showMatViews)
 		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_MATVIEW) ",");
 	if (showIndexes)
-		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_INDEX) ",");
+		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_INDEX) ","
+							 CppAsString2(RELKIND_PARTITIONED_INDEX) ",");
 	if (showSeq)
 		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_SEQUENCE) ",");
 	if (showSystem || pattern)
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b51098deca127f108389bb2081e45be85c3b4fa8..8bc4a194a53367c164b13b7f607f0e6820e90147 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -412,7 +412,8 @@ static const SchemaQuery Query_for_list_of_indexes = {
 	/* catname */
 	"pg_catalog.pg_class c",
 	/* selcondition */
-	"c.relkind IN (" CppAsString2(RELKIND_INDEX) ")",
+	"c.relkind IN (" CppAsString2(RELKIND_INDEX) ", "
+	CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
 	/* viscondition */
 	"pg_catalog.pg_table_is_visible(c.oid)",
 	/* namespace */
@@ -600,6 +601,23 @@ static const SchemaQuery Query_for_list_of_tmf = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_tpm = {
+	/* catname */
+	"pg_catalog.pg_class c",
+	/* selcondition */
+	"c.relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+	CppAsString2(RELKIND_PARTITIONED_TABLE) ", "
+	CppAsString2(RELKIND_MATVIEW) ")",
+	/* viscondition */
+	"pg_catalog.pg_table_is_visible(c.oid)",
+	/* namespace */
+	"c.relnamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.relname)",
+	/* qualresult */
+	NULL
+};
+
 static const SchemaQuery Query_for_list_of_tm = {
 	/* catname */
 	"pg_catalog.pg_class c",
@@ -1676,7 +1694,12 @@ psql_completion(const char *text, int start, int end)
 								   "UNION SELECT 'ALL IN TABLESPACE'");
 	/* ALTER INDEX <name> */
 	else if (Matches3("ALTER", "INDEX", MatchAny))
-		COMPLETE_WITH_LIST5("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET", "RESET");
+		COMPLETE_WITH_LIST6("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET",
+							"RESET", "ATTACH PARTITION");
+	else if (Matches4("ALTER", "INDEX", MatchAny, "ATTACH"))
+		COMPLETE_WITH_CONST("PARTITION");
+	else if (Matches5("ALTER", "INDEX", MatchAny, "ATTACH", "PARTITION"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
 	/* ALTER INDEX <name> ALTER COLUMN <colnum> */
 	else if (Matches6("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN", MatchAny))
 		COMPLETE_WITH_CONST("SET STATISTICS");
@@ -2338,10 +2361,13 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
 								   " UNION SELECT 'ON'"
 								   " UNION SELECT 'CONCURRENTLY'");
-	/* Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of tables  */
+	/*
+	 * Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of relations
+	 * that can indexes can be created on
+	 */
 	else if (TailMatches3("INDEX|CONCURRENTLY", MatchAny, "ON") ||
 			 TailMatches2("INDEX|CONCURRENTLY", "ON"))
-		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, NULL);
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tpm, NULL);
 
 	/*
 	 * Complete CREATE|UNIQUE INDEX CONCURRENTLY with "ON" and existing
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 6f290d5c6f1160cbd0320771ba59ce9179250fca..46c271a46c67fb0203cbb21b158aa74667482e95 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -49,6 +49,20 @@
  * Example: a trigger that's created to enforce a foreign-key constraint
  * is made internally dependent on the constraint's pg_constraint entry.
  *
+ * DEPENDENCY_INTERNAL_AUTO ('I'): the dependent object was created as
+ * part of creation of the referenced object, and is really just a part
+ * of its internal implementation.  A DROP of the dependent object will
+ * be disallowed outright (we'll tell the user to issue a DROP against the
+ * referenced object, instead).  While a regular internal dependency will
+ * prevent the dependent object from being dropped while any such
+ * dependencies remain, DEPENDENCY_INTERNAL_AUTO will allow such a drop as
+ * long as the object can be found by following any of such dependencies.
+ * Example: an index on a partition is made internal-auto-dependent on
+ * both the partition itself as well as on the index on the parent
+ * partitioned table; so the partition index is dropped together with
+ * either the partition it indexes, or with the parent index it is attached
+ * to.
+
  * DEPENDENCY_EXTENSION ('e'): the dependent object is a member of the
  * extension that is the referenced object.  The dependent object can be
  * dropped only via DROP EXTENSION on the referenced object.  Functionally
@@ -75,6 +89,7 @@ typedef enum DependencyType
 	DEPENDENCY_NORMAL = 'n',
 	DEPENDENCY_AUTO = 'a',
 	DEPENDENCY_INTERNAL = 'i',
+	DEPENDENCY_INTERNAL_AUTO = 'I',
 	DEPENDENCY_EXTENSION = 'e',
 	DEPENDENCY_AUTO_EXTENSION = 'x',
 	DEPENDENCY_PIN = 'p'
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 4790f0c735f4c258b68981437b130bd43f841937..235e180299c1cbd6d963052a33a863aa3e1224d0 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -47,10 +47,13 @@ extern void index_check_primary_key(Relation heapRel,
 #define	INDEX_CREATE_SKIP_BUILD				(1 << 2)
 #define	INDEX_CREATE_CONCURRENT				(1 << 3)
 #define	INDEX_CREATE_IF_NOT_EXISTS			(1 << 4)
+#define	INDEX_CREATE_PARTITIONED			(1 << 5)
+#define INDEX_CREATE_INVALID				(1 << 6)
 
 extern Oid index_create(Relation heapRelation,
 			 const char *indexRelationName,
 			 Oid indexRelationId,
+			 Oid parentIndexRelid,
 			 Oid relFileNode,
 			 IndexInfo *indexInfo,
 			 List *indexColNames,
@@ -84,6 +87,11 @@ extern void index_drop(Oid indexId, bool concurrent);
 
 extern IndexInfo *BuildIndexInfo(Relation index);
 
+extern bool CompareIndexInfo(IndexInfo *info1, IndexInfo *info2,
+				 Oid *collations1, Oid *collations2,
+				 Oid *opfamilies1, Oid *opfamilies2,
+				 AttrNumber *attmap, int maplen);
+
 extern void BuildSpeculativeIndexInfo(Relation index, IndexInfo *ii);
 
 extern void FormIndexDatum(IndexInfo *indexInfo,
@@ -138,4 +146,6 @@ extern Size EstimateReindexStateSpace(void);
 extern void SerializeReindexState(Size maxsize, char *start_address);
 extern void RestoreReindexState(void *reindexstate);
 
+extern void IndexSetParentIndex(Relation idx, Oid parentOid);
+
 #endif							/* INDEX_H */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index e7049438eb5266d7552e21a52a1e9cedc052e75d..26b1866c69c2ac493fa83cc22ddddb35b2c1d0c1 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -166,6 +166,7 @@ DESCR("");
 #define		  RELKIND_COMPOSITE_TYPE  'c'	/* composite type */
 #define		  RELKIND_FOREIGN_TABLE   'f'	/* foreign table */
 #define		  RELKIND_PARTITIONED_TABLE 'p' /* partitioned table */
+#define		  RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */
 
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
diff --git a/src/include/catalog/pg_inherits_fn.h b/src/include/catalog/pg_inherits_fn.h
index 405af230d14c35b0bb159fe60ec69d50824b317c..eebee977a559607a1479ee4ac03789c118ec79e3 100644
--- a/src/include/catalog/pg_inherits_fn.h
+++ b/src/include/catalog/pg_inherits_fn.h
@@ -23,5 +23,8 @@ extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
 extern bool has_subclass(Oid relationId);
 extern bool has_superclass(Oid relationId);
 extern bool typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId);
+extern void StoreSingleInheritance(Oid relationId, Oid parentOid,
+					   int32 seqNumber);
+extern bool DeleteInheritsTuple(Oid inhrelid, Oid inhparent);
 
 #endif							/* PG_INHERITS_FN_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 1f18cad96331e638e5104a8129577aebacaaf55d..41007162aa54389cf7e2967cf3fefd408f5c919e 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -25,12 +25,13 @@ extern void RemoveObjects(DropStmt *stmt);
 extern ObjectAddress DefineIndex(Oid relationId,
 			IndexStmt *stmt,
 			Oid indexRelationId,
+			Oid parentIndexId,
 			bool is_alter_table,
 			bool check_rights,
 			bool check_not_in_use,
 			bool skip_build,
 			bool quiet);
-extern Oid	ReindexIndex(RangeVar *indexRelation, int options);
+extern void ReindexIndex(RangeVar *indexRelation, int options);
 extern Oid	ReindexTable(RangeVar *relation, int options);
 extern void ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 					  int options);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4bb5cb163d7e913c3344f714fba45b148eb109fb..63a75bd5eda00a04bc9787a4204b0107750cd08a 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -158,6 +158,7 @@ typedef struct IndexInfo
 	bool		ii_ReadyForInserts;
 	bool		ii_Concurrent;
 	bool		ii_BrokenHotChain;
+	Oid			ii_Am;
 	void	   *ii_AmCache;
 	MemoryContext ii_Context;
 } IndexInfo;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b72178efd19df2720fb850e5745b8ba3ed12a8a0..0296784726f05f6cc17507b1f0b28c8d7d9a7a81 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -839,7 +839,7 @@ typedef struct PartitionRangeDatum
 } PartitionRangeDatum;
 
 /*
- * PartitionCmd - info for ALTER TABLE ATTACH/DETACH PARTITION commands
+ * PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION commands
  */
 typedef struct PartitionCmd
 {
@@ -2702,6 +2702,10 @@ typedef struct FetchStmt
  * index, just a UNIQUE/PKEY constraint using an existing index.  isconstraint
  * must always be true in this case, and the fields describing the index
  * properties are empty.
+ *
+ * The relation to build the index on can be represented either by name
+ * (in which case the RangeVar indicates whether to recurse or not) or by OID
+ * (in which case the command is always recursive).
  * ----------------------
  */
 typedef struct IndexStmt
@@ -2709,6 +2713,7 @@ typedef struct IndexStmt
 	NodeTag		type;
 	char	   *idxname;		/* name of new index, or NULL for default */
 	RangeVar   *relation;		/* relation to build index on */
+	Oid			relationId;		/* OID of relation to build index on */
 	char	   *accessMethod;	/* name of access method (eg. btree) */
 	char	   *tableSpace;		/* tablespace, or NULL for default */
 	List	   *indexParams;	/* columns to index: a list of IndexElem */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index a7f5e0caeae2e6677e0ce6890de1fed7c97c3a3d..64aa8234e5fd564dd27ad68c8dea970c8cb0ffb3 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -27,5 +27,8 @@ extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
 extern List *transformCreateSchemaStmt(CreateSchemaStmt *stmt);
 extern PartitionBoundSpec *transformPartitionBound(ParseState *pstate, Relation parent,
 						PartitionBoundSpec *spec);
+extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel, Oid heapOid,
+						Relation source_idx,
+						const AttrNumber *attmap, int attmap_length);
 
 #endif							/* PARSE_UTILCMD_H */
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 11f0baa11b0eb9cd71fd88b00cd7174ccf1057ab..517fb080bd58c58a601b65166f9e82f627456bfa 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -1965,6 +1965,67 @@ create table tab1 (a int, b text);
 create table tab2 (x int, y tab1);
 alter table tab1 alter column b type varchar; -- fails
 ERROR:  cannot alter table "tab1" because column "tab2.y" uses its row type
+-- Alter column type that's part of a partitioned index
+create table at_partitioned (a int, b text) partition by range (a);
+create table at_part_1 partition of at_partitioned for values from (0) to (1000);
+insert into at_partitioned values (512, '0.123');
+create table at_part_2 (b text, a int);
+insert into at_part_2 values ('1.234', 1024);
+create index on at_partitioned (b);
+create index on at_partitioned (a);
+\d at_part_1
+             Table "public.at_part_1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+Partition of: at_partitioned FOR VALUES FROM (0) TO (1000)
+Indexes:
+    "at_part_1_a_idx" btree (a)
+    "at_part_1_b_idx" btree (b)
+
+\d at_part_2
+             Table "public.at_part_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ b      | text    |           |          | 
+ a      | integer |           |          | 
+
+alter table at_partitioned attach partition at_part_2 for values from (1000) to (2000);
+\d at_part_2
+             Table "public.at_part_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ b      | text    |           |          | 
+ a      | integer |           |          | 
+Partition of: at_partitioned FOR VALUES FROM (1000) TO (2000)
+Indexes:
+    "at_part_2_a_idx" btree (a)
+    "at_part_2_b_idx" btree (b)
+
+alter table at_partitioned alter column b type numeric using b::numeric;
+\d at_part_1
+             Table "public.at_part_1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | numeric |           |          | 
+Partition of: at_partitioned FOR VALUES FROM (0) TO (1000)
+Indexes:
+    "at_part_1_a_idx" btree (a)
+    "at_part_1_b_idx" btree (b)
+
+\d at_part_2
+             Table "public.at_part_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ b      | numeric |           |          | 
+ a      | integer |           |          | 
+Partition of: at_partitioned FOR VALUES FROM (1000) TO (2000)
+Indexes:
+    "at_part_2_a_idx" btree (a)
+    "at_part_2_b_idx" btree (b)
+
 -- disallow recursive containment of row types
 create temp table recur1 (f1 int);
 alter table recur1 add column f2 recur1; -- fails
@@ -3276,7 +3337,7 @@ CREATE TABLE unparted (
 );
 CREATE TABLE fail_part (like unparted);
 ALTER TABLE unparted ATTACH PARTITION fail_part FOR VALUES IN ('a');
-ERROR:  "unparted" is not partitioned
+ERROR:  table "unparted" is not partitioned
 DROP TABLE unparted, fail_part;
 -- check that partition bound is compatible
 CREATE TABLE list_parted (
@@ -3656,7 +3717,7 @@ DROP TABLE fail_part;
 -- check that the table is partitioned at all
 CREATE TABLE regular_table (a int);
 ALTER TABLE regular_table DETACH PARTITION any_name;
-ERROR:  "regular_table" is not partitioned
+ERROR:  table "regular_table" is not partitioned
 DROP TABLE regular_table;
 -- check that the partition being detached exists at all
 ALTER TABLE list_parted2 DETACH PARTITION part_4;
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
new file mode 100644
index 0000000000000000000000000000000000000000..e9cccca8760730982868aae8de392da1bdb14f8b
--- /dev/null
+++ b/src/test/regress/expected/indexing.out
@@ -0,0 +1,757 @@
+-- Creating an index on a partitioned table makes the partitions
+-- automatically get the index
+create table idxpart (a int, b int, c text) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+create table idxpart2 partition of idxpart for values from (10) to (100)
+	partition by range (b);
+create table idxpart21 partition of idxpart2 for values from (0) to (100);
+create index on idxpart (a);
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+     relname     | relkind |   inhparent    
+-----------------+---------+----------------
+ idxpart         | p       | 
+ idxpart1        | r       | 
+ idxpart1_a_idx  | i       | idxpart_a_idx
+ idxpart2        | p       | 
+ idxpart21       | r       | 
+ idxpart21_a_idx | i       | idxpart2_a_idx
+ idxpart2_a_idx  | I       | idxpart_a_idx
+ idxpart_a_idx   | I       | 
+(8 rows)
+
+drop table idxpart;
+-- Some unsupported features
+create table idxpart (a int, b int, c text) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+create unique index on idxpart (a);
+ERROR:  cannot create unique index on partitioned table "idxpart"
+create index concurrently on idxpart (a);
+ERROR:  cannot create index on partitioned table "idxpart" concurrently
+drop table idxpart;
+-- If a table without index is attached as partition to a table with
+-- an index, the index is automatically created
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (b, c);
+create table idxpart1 (like idxpart);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (10)
+Indexes:
+    "idxpart1_a_idx" btree (a)
+    "idxpart1_b_c_idx" btree (b, c)
+
+drop table idxpart;
+-- If a partition already has an index, don't create a duplicative one
+create table idxpart (a int, b int) partition by range (a, b);
+create table idxpart1 partition of idxpart for values from (0, 0) to (10, 10);
+create index on idxpart1 (a, b);
+create index on idxpart (a, b);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (0, 0) TO (10, 10)
+Indexes:
+    "idxpart1_a_b_idx" btree (a, b)
+
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+     relname      | relkind |    inhparent    
+------------------+---------+-----------------
+ idxpart          | p       | 
+ idxpart1         | r       | 
+ idxpart1_a_b_idx | i       | idxpart_a_b_idx
+ idxpart_a_b_idx  | I       | 
+(4 rows)
+
+drop table idxpart;
+-- DROP behavior for partitioned indexes
+create table idxpart (a int) partition by range (a);
+create index on idxpart (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+drop index idxpart1_a_idx;	-- no way
+ERROR:  cannot drop index idxpart1_a_idx because index idxpart_a_idx requires it
+HINT:  You can drop index idxpart_a_idx instead.
+drop index idxpart_a_idx;	-- both indexes go away
+select relname, relkind from pg_class
+  where relname like 'idxpart%' order by relname;
+ relname  | relkind 
+----------+---------
+ idxpart  | p
+ idxpart1 | r
+(2 rows)
+
+create index on idxpart (a);
+drop table idxpart1;		-- the index on partition goes away too
+select relname, relkind from pg_class
+  where relname like 'idxpart%' order by relname;
+    relname    | relkind 
+---------------+---------
+ idxpart       | p
+ idxpart_a_idx | I
+(2 rows)
+
+drop table idxpart;
+-- ALTER INDEX .. ATTACH, error cases
+create table idxpart (a int, b int) partition by range (a, b);
+create table idxpart1 partition of idxpart for values from (0, 0) to (10, 10);
+create index idxpart_a_b_idx on only idxpart (a, b);
+create index idxpart1_a_b_idx on idxpart1 (a, b);
+create index idxpart1_tst1 on idxpart1 (b, a);
+create index idxpart1_tst2 on idxpart1 using hash (a);
+create index idxpart1_tst3 on idxpart1 (a, b) where a > 10;
+alter index idxpart attach partition idxpart1;
+ERROR:  "idxpart" is not an index
+alter index idxpart_a_b_idx attach partition idxpart1;
+ERROR:  "idxpart1" is not an index
+alter index idxpart_a_b_idx attach partition idxpart_a_b_idx;
+ERROR:  cannot attach index "idxpart_a_b_idx" as a partition of index "idxpart_a_b_idx"
+DETAIL:  Index "idxpart_a_b_idx" is not an index on any partition of table "idxpart".
+alter index idxpart_a_b_idx attach partition idxpart1_b_idx;
+ERROR:  relation "idxpart1_b_idx" does not exist
+alter index idxpart_a_b_idx attach partition idxpart1_tst1;
+ERROR:  cannot attach index "idxpart1_tst1" as a partition of index "idxpart_a_b_idx"
+DETAIL:  The index definitions do not match.
+alter index idxpart_a_b_idx attach partition idxpart1_tst2;
+ERROR:  cannot attach index "idxpart1_tst2" as a partition of index "idxpart_a_b_idx"
+DETAIL:  The index definitions do not match.
+alter index idxpart_a_b_idx attach partition idxpart1_tst3;
+ERROR:  cannot attach index "idxpart1_tst3" as a partition of index "idxpart_a_b_idx"
+DETAIL:  The index definitions do not match.
+-- OK
+alter index idxpart_a_b_idx attach partition idxpart1_a_b_idx;
+alter index idxpart_a_b_idx attach partition idxpart1_a_b_idx; -- quiet
+-- reject dupe
+create index idxpart1_2_a_b on idxpart1 (a, b);
+alter index idxpart_a_b_idx attach partition idxpart1_2_a_b;
+ERROR:  cannot attach index "idxpart1_2_a_b" as a partition of index "idxpart_a_b_idx"
+DETAIL:  Another index is already attached for partition "idxpart1".
+drop table idxpart;
+-- make sure everything's gone
+select indexrelid::regclass, indrelid::regclass
+  from pg_index where indexrelid::regclass::text like 'idxpart%';
+ indexrelid | indrelid 
+------------+----------
+(0 rows)
+
+-- Don't auto-attach incompatible indexes
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 (a int, b int);
+create index on idxpart1 using hash (a);
+create index on idxpart1 (a) where b > 1;
+create index on idxpart1 ((a + 0));
+create index on idxpart1 (a, a);
+create index on idxpart (a);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (1000)
+Indexes:
+    "idxpart1_a_a1_idx" btree (a, a)
+    "idxpart1_a_idx" hash (a)
+    "idxpart1_a_idx1" btree (a) WHERE b > 1
+    "idxpart1_a_idx2" btree (a)
+    "idxpart1_expr_idx" btree ((a + 0))
+
+drop table idxpart;
+-- If CREATE INDEX ONLY, don't create indexes on partitions; and existing
+-- indexes on partitions don't change parent.  ALTER INDEX ATTACH can change
+-- the parent after the fact.
+create table idxpart (a int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (100);
+create table idxpart2 partition of idxpart for values from (100) to (1000)
+  partition by range (a);
+create table idxpart21 partition of idxpart2 for values from (100) to (200);
+create table idxpart22 partition of idxpart2 for values from (200) to (300);
+create index on idxpart22 (a);
+create index on only idxpart2 (a);
+create index on idxpart (a);
+-- Here we expect that idxpart1 and idxpart2 have a new index, but idxpart21
+-- does not; also, idxpart22 is not attached.
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (100)
+Indexes:
+    "idxpart1_a_idx" btree (a)
+
+\d idxpart2
+              Table "public.idxpart2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (100) TO (1000)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart2_a_idx" btree (a) INVALID
+Number of partitions: 2 (Use \d+ to list them.)
+
+\d idxpart21
+             Table "public.idxpart21"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart2 FOR VALUES FROM (100) TO (200)
+
+select indexrelid::regclass, indrelid::regclass, inhparent::regclass
+  from pg_index idx left join pg_inherits inh on (idx.indexrelid = inh.inhrelid)
+where indexrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+   indexrelid    | indrelid  |   inhparent   
+-----------------+-----------+---------------
+ idxpart_a_idx   | idxpart   | 
+ idxpart1_a_idx  | idxpart1  | idxpart_a_idx
+ idxpart2_a_idx  | idxpart2  | idxpart_a_idx
+ idxpart22_a_idx | idxpart22 | 
+(4 rows)
+
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+select indexrelid::regclass, indrelid::regclass, inhparent::regclass
+  from pg_index idx left join pg_inherits inh on (idx.indexrelid = inh.inhrelid)
+where indexrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+   indexrelid    | indrelid  |   inhparent    
+-----------------+-----------+----------------
+ idxpart_a_idx   | idxpart   | 
+ idxpart1_a_idx  | idxpart1  | idxpart_a_idx
+ idxpart2_a_idx  | idxpart2  | idxpart_a_idx
+ idxpart22_a_idx | idxpart22 | idxpart2_a_idx
+(4 rows)
+
+-- attaching idxpart22 is not enough to set idxpart22_a_idx valid ...
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+\d idxpart2
+              Table "public.idxpart2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (100) TO (1000)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart2_a_idx" btree (a) INVALID
+Number of partitions: 2 (Use \d+ to list them.)
+
+-- ... but this one is.
+create index on idxpart21 (a);
+alter index idxpart2_a_idx attach partition idxpart21_a_idx;
+\d idxpart2
+              Table "public.idxpart2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (100) TO (1000)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart2_a_idx" btree (a)
+Number of partitions: 2 (Use \d+ to list them.)
+
+drop table idxpart;
+-- When a table is attached a partition and it already has an index, a
+-- duplicate index should not get created, but rather the index becomes
+-- attached to the parent's index.
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (b, c);
+create table idxpart1 (like idxpart including indexes);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Indexes:
+    "idxpart1_a_idx" btree (a)
+    "idxpart1_b_c_idx" btree (b, c)
+
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+     relname      | relkind | inhparent 
+------------------+---------+-----------
+ idxpart          | p       | 
+ idxpart1         | r       | 
+ idxpart1_a_idx   | i       | 
+ idxpart1_b_c_idx | i       | 
+ idxparti         | I       | 
+ idxparti2        | I       | 
+(6 rows)
+
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (10)
+Indexes:
+    "idxpart1_a_idx" btree (a)
+    "idxpart1_b_c_idx" btree (b, c)
+
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+     relname      | relkind | inhparent 
+------------------+---------+-----------
+ idxpart          | p       | 
+ idxpart1         | r       | 
+ idxpart1_a_idx   | i       | idxparti
+ idxpart1_b_c_idx | i       | idxparti2
+ idxparti         | I       | 
+ idxparti2        | I       | 
+(6 rows)
+
+drop table idxpart;
+-- Verify that attaching an invalid index does not mark the parent index valid.
+-- On the other hand, attaching a valid index marks not only its direct
+-- ancestor valid, but also any indirect ancestor that was only missing the one
+-- that was just made valid
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (1) to (1000) partition by range (a);
+create table idxpart11 partition of idxpart1 for values from (1) to (100);
+create index on only idxpart1 (a);
+create index on only idxpart (a);
+-- this results in two invalid indexes:
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+    relname     | indisvalid 
+----------------+------------
+ idxpart1_a_idx | f
+ idxpart_a_idx  | f
+(2 rows)
+
+-- idxpart1_a_idx is not valid, so idxpart_a_idx should not become valid:
+alter index idxpart_a_idx attach partition idxpart1_a_idx;
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+    relname     | indisvalid 
+----------------+------------
+ idxpart1_a_idx | f
+ idxpart_a_idx  | f
+(2 rows)
+
+-- after creating and attaching this, both idxpart1_a_idx and idxpart_a_idx
+-- should become valid
+create index on idxpart11 (a);
+alter index idxpart1_a_idx attach partition idxpart11_a_idx;
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+     relname     | indisvalid 
+-----------------+------------
+ idxpart11_a_idx | t
+ idxpart1_a_idx  | t
+ idxpart_a_idx   | t
+(3 rows)
+
+drop table idxpart;
+-- verify dependency handling during ALTER TABLE DETACH PARTITION
+create table idxpart (a int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 (a);
+create index on idxpart (a);
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+    relname     | relkind 
+----------------+---------
+ idxpart        | p
+ idxpart1       | r
+ idxpart1_a_idx | i
+ idxpart2       | r
+ idxpart2_a_idx | i
+ idxpart3       | r
+ idxpart3_a_idx | i
+ idxpart_a_idx  | I
+(8 rows)
+
+-- a) after detaching partitions, the indexes can be dropped independently
+alter table idxpart detach partition idxpart1;
+alter table idxpart detach partition idxpart2;
+alter table idxpart detach partition idxpart3;
+drop index idxpart1_a_idx;
+drop index idxpart2_a_idx;
+drop index idxpart3_a_idx;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+    relname    | relkind 
+---------------+---------
+ idxpart       | p
+ idxpart1      | r
+ idxpart2      | r
+ idxpart3      | r
+ idxpart_a_idx | I
+(5 rows)
+
+drop table idxpart, idxpart1, idxpart2, idxpart3;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+ relname | relkind 
+---------+---------
+(0 rows)
+
+create table idxpart (a int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 (a);
+create index on idxpart (a);
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+-- b) after detaching, dropping the index on parent does not remove the others
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+    relname     | relkind 
+----------------+---------
+ idxpart        | p
+ idxpart1       | r
+ idxpart1_a_idx | i
+ idxpart2       | r
+ idxpart2_a_idx | i
+ idxpart3       | r
+ idxpart3_a_idx | i
+ idxpart_a_idx  | I
+(8 rows)
+
+alter table idxpart detach partition idxpart1;
+alter table idxpart detach partition idxpart2;
+alter table idxpart detach partition idxpart3;
+drop index idxpart_a_idx;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+    relname     | relkind 
+----------------+---------
+ idxpart        | p
+ idxpart1       | r
+ idxpart1_a_idx | i
+ idxpart2       | r
+ idxpart2_a_idx | i
+ idxpart3       | r
+ idxpart3_a_idx | i
+(7 rows)
+
+drop table idxpart, idxpart1, idxpart2, idxpart3;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+ relname | relkind 
+---------+---------
+(0 rows)
+
+-- Verify that expression indexes inherit correctly
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 ((a + b));
+create index on idxpart ((a + b));
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+       child       |      parent      |                              childdef                              
+-------------------+------------------+--------------------------------------------------------------------
+ idxpart1_expr_idx | idxpart_expr_idx | CREATE INDEX idxpart1_expr_idx ON idxpart1 USING btree (((a + b)))
+ idxpart2_expr_idx | idxpart_expr_idx | CREATE INDEX idxpart2_expr_idx ON idxpart2 USING btree (((a + b)))
+ idxpart3_expr_idx | idxpart_expr_idx | CREATE INDEX idxpart3_expr_idx ON idxpart3 USING btree (((a + b)))
+(3 rows)
+
+drop table idxpart;
+-- Verify behavior for collation (mis)matches
+create table idxpart (a text) partition by range (a);
+create table idxpart1 (like idxpart);
+create table idxpart2 (like idxpart);
+create index on idxpart2 (a collate "POSIX");
+create index on idxpart2 (a);
+create index on idxpart2 (a collate "C");
+alter table idxpart attach partition idxpart1 for values from ('aaa') to ('bbb');
+alter table idxpart attach partition idxpart2 for values from ('bbb') to ('ccc');
+create table idxpart3 partition of idxpart for values from ('ccc') to ('ddd');
+create index on idxpart (a collate "C");
+create table idxpart4 partition of idxpart for values from ('ddd') to ('eee');
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+      child      |    parent     |                                childdef                                 
+-----------------+---------------+-------------------------------------------------------------------------
+ idxpart1_a_idx  | idxpart_a_idx | CREATE INDEX idxpart1_a_idx ON idxpart1 USING btree (a COLLATE "C")
+ idxpart2_a_idx  |               | CREATE INDEX idxpart2_a_idx ON idxpart2 USING btree (a COLLATE "POSIX")
+ idxpart2_a_idx1 |               | CREATE INDEX idxpart2_a_idx1 ON idxpart2 USING btree (a)
+ idxpart2_a_idx2 | idxpart_a_idx | CREATE INDEX idxpart2_a_idx2 ON idxpart2 USING btree (a COLLATE "C")
+ idxpart3_a_idx  | idxpart_a_idx | CREATE INDEX idxpart3_a_idx ON idxpart3 USING btree (a COLLATE "C")
+ idxpart4_a_idx  | idxpart_a_idx | CREATE INDEX idxpart4_a_idx ON idxpart4 USING btree (a COLLATE "C")
+ idxpart_a_idx   |               | CREATE INDEX idxpart_a_idx ON ONLY idxpart USING btree (a COLLATE "C")
+(7 rows)
+
+drop table idxpart;
+-- Verify behavior for opclass (mis)matches
+create table idxpart (a text) partition by range (a);
+create table idxpart1 (like idxpart);
+create table idxpart2 (like idxpart);
+create index on idxpart2 (a);
+alter table idxpart attach partition idxpart1 for values from ('aaa') to ('bbb');
+alter table idxpart attach partition idxpart2 for values from ('bbb') to ('ccc');
+create table idxpart3 partition of idxpart for values from ('ccc') to ('ddd');
+create index on idxpart (a text_pattern_ops);
+create table idxpart4 partition of idxpart for values from ('ddd') to ('eee');
+-- must *not* have attached the index we created on idxpart2
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+      child      |    parent     |                                  childdef                                   
+-----------------+---------------+-----------------------------------------------------------------------------
+ idxpart1_a_idx  | idxpart_a_idx | CREATE INDEX idxpart1_a_idx ON idxpart1 USING btree (a text_pattern_ops)
+ idxpart2_a_idx  |               | CREATE INDEX idxpart2_a_idx ON idxpart2 USING btree (a)
+ idxpart2_a_idx1 | idxpart_a_idx | CREATE INDEX idxpart2_a_idx1 ON idxpart2 USING btree (a text_pattern_ops)
+ idxpart3_a_idx  | idxpart_a_idx | CREATE INDEX idxpart3_a_idx ON idxpart3 USING btree (a text_pattern_ops)
+ idxpart4_a_idx  | idxpart_a_idx | CREATE INDEX idxpart4_a_idx ON idxpart4 USING btree (a text_pattern_ops)
+ idxpart_a_idx   |               | CREATE INDEX idxpart_a_idx ON ONLY idxpart USING btree (a text_pattern_ops)
+(6 rows)
+
+drop index idxpart_a_idx;
+create index on only idxpart (a text_pattern_ops);
+-- must reject
+alter index idxpart_a_idx attach partition idxpart2_a_idx;
+ERROR:  cannot attach index "idxpart2_a_idx" as a partition of index "idxpart_a_idx"
+DETAIL:  The index definitions do not match.
+drop table idxpart;
+-- Verify that attaching indexes maps attribute numbers correctly
+create table idxpart (col1 int, a int, col2 int, b int) partition by range (a);
+create table idxpart1 (b int, col1 int, col2 int, col3 int, a int);
+alter table idxpart drop column col1, drop column col2;
+alter table idxpart1 drop column col1, drop column col2, drop column col3;
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+create index idxpart_1_idx on only idxpart (b, a);
+create index idxpart1_1_idx on idxpart1 (b, a);
+create index idxpart1_1b_idx on idxpart1 (b);
+-- test expressions and partial-index predicate, too
+create index idxpart_2_idx on only idxpart ((b + a)) where a > 1;
+create index idxpart1_2_idx on idxpart1 ((b + a)) where a > 1;
+create index idxpart1_2b_idx on idxpart1 ((a + b)) where a > 1;
+create index idxpart1_2c_idx on idxpart1 ((b + a)) where b > 1;
+alter index idxpart_1_idx attach partition idxpart1_1b_idx;	-- fail
+ERROR:  cannot attach index "idxpart1_1b_idx" as a partition of index "idxpart_1_idx"
+DETAIL:  The index definitions do not match.
+alter index idxpart_1_idx attach partition idxpart1_1_idx;
+alter index idxpart_2_idx attach partition idxpart1_2b_idx;	-- fail
+ERROR:  cannot attach index "idxpart1_2b_idx" as a partition of index "idxpart_2_idx"
+DETAIL:  The index definitions do not match.
+alter index idxpart_2_idx attach partition idxpart1_2c_idx;	-- fail
+ERROR:  cannot attach index "idxpart1_2c_idx" as a partition of index "idxpart_2_idx"
+DETAIL:  The index definitions do not match.
+alter index idxpart_2_idx attach partition idxpart1_2_idx;	-- ok
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+      child      |    parent     |                                     childdef                                     
+-----------------+---------------+----------------------------------------------------------------------------------
+ idxpart1_1_idx  | idxpart_1_idx | CREATE INDEX idxpart1_1_idx ON idxpart1 USING btree (b, a)
+ idxpart1_1b_idx |               | CREATE INDEX idxpart1_1b_idx ON idxpart1 USING btree (b)
+ idxpart1_2_idx  | idxpart_2_idx | CREATE INDEX idxpart1_2_idx ON idxpart1 USING btree (((b + a))) WHERE (a > 1)
+ idxpart1_2b_idx |               | CREATE INDEX idxpart1_2b_idx ON idxpart1 USING btree (((a + b))) WHERE (a > 1)
+ idxpart1_2c_idx |               | CREATE INDEX idxpart1_2c_idx ON idxpart1 USING btree (((b + a))) WHERE (b > 1)
+ idxpart_1_idx   |               | CREATE INDEX idxpart_1_idx ON ONLY idxpart USING btree (b, a)
+ idxpart_2_idx   |               | CREATE INDEX idxpart_2_idx ON ONLY idxpart USING btree (((b + a))) WHERE (a > 1)
+(7 rows)
+
+drop table idxpart;
+-- Make sure the partition columns are mapped correctly
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (c, b);
+create table idxpart1 (c text, a int, b int);
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+create table idxpart2 (c text, a int, b int);
+create index on idxpart2 (a);
+create index on idxpart2 (c, b);
+alter table idxpart attach partition idxpart2 for values from (10) to (20);
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+     relname      |                       pg_get_indexdef                        
+------------------+--------------------------------------------------------------
+ idxparti         | CREATE INDEX idxparti ON ONLY idxpart USING btree (a)
+ idxparti2        | CREATE INDEX idxparti2 ON ONLY idxpart USING btree (c, b)
+ idxpart1_a_idx   | CREATE INDEX idxpart1_a_idx ON idxpart1 USING btree (a)
+ idxpart1_c_b_idx | CREATE INDEX idxpart1_c_b_idx ON idxpart1 USING btree (c, b)
+ idxpart2_a_idx   | CREATE INDEX idxpart2_a_idx ON idxpart2 USING btree (a)
+ idxpart2_c_b_idx | CREATE INDEX idxpart2_c_b_idx ON idxpart2 USING btree (c, b)
+(6 rows)
+
+drop table idxpart;
+-- Verify that columns are mapped correctly in expression indexes
+create table idxpart (col1 int, col2 int, a int, b int) partition by range (a);
+create table idxpart1 (col2 int, b int, col1 int, a int);
+create table idxpart2 (col1 int, col2 int, b int, a int);
+alter table idxpart drop column col1, drop column col2;
+alter table idxpart1 drop column col1, drop column col2;
+alter table idxpart2 drop column col1, drop column col2;
+create index on idxpart2 (abs(b));
+alter table idxpart attach partition idxpart2 for values from (0) to (1);
+create index on idxpart (abs(b));
+alter table idxpart attach partition idxpart1 for values from (1) to (2);
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+     relname      |                          pg_get_indexdef                          
+------------------+-------------------------------------------------------------------
+ idxpart_abs_idx  | CREATE INDEX idxpart_abs_idx ON ONLY idxpart USING btree (abs(b))
+ idxpart1_abs_idx | CREATE INDEX idxpart1_abs_idx ON idxpart1 USING btree (abs(b))
+ idxpart2_abs_idx | CREATE INDEX idxpart2_abs_idx ON idxpart2 USING btree (abs(b))
+(3 rows)
+
+drop table idxpart;
+-- Verify that columns are mapped correctly for WHERE in a partial index
+create table idxpart (col1 int, a int, col3 int, b int) partition by range (a);
+alter table idxpart drop column col1, drop column col3;
+create table idxpart1 (col1 int, col2 int, col3 int, col4 int, b int, a int);
+alter table idxpart1 drop column col1, drop column col2, drop column col3, drop column col4;
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+create table idxpart2 (col1 int, col2 int, b int, a int);
+create index on idxpart2 (a) where b > 1000;
+alter table idxpart2 drop column col1, drop column col2;
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create index on idxpart (a) where b > 1000;
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+    relname     |                               pg_get_indexdef                               
+----------------+-----------------------------------------------------------------------------
+ idxpart_a_idx  | CREATE INDEX idxpart_a_idx ON ONLY idxpart USING btree (a) WHERE (b > 1000)
+ idxpart1_a_idx | CREATE INDEX idxpart1_a_idx ON idxpart1 USING btree (a) WHERE (b > 1000)
+ idxpart2_a_idx | CREATE INDEX idxpart2_a_idx ON idxpart2 USING btree (a) WHERE (b > 1000)
+(3 rows)
+
+drop table idxpart;
+-- Column number mapping: dropped columns in the partition
+create table idxpart1 (drop_1 int, drop_2 int, col_keep int, drop_3 int);
+alter table idxpart1 drop column drop_1;
+alter table idxpart1 drop column drop_2;
+alter table idxpart1 drop column drop_3;
+create index on idxpart1 (col_keep);
+create table idxpart (col_keep int) partition by range (col_keep);
+create index on idxpart (col_keep);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart
+               Table "public.idxpart"
+  Column  |  Type   | Collation | Nullable | Default 
+----------+---------+-----------+----------+---------
+ col_keep | integer |           |          | 
+Partition key: RANGE (col_keep)
+Indexes:
+    "idxpart_col_keep_idx" btree (col_keep)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart1
+               Table "public.idxpart1"
+  Column  |  Type   | Collation | Nullable | Default 
+----------+---------+-----------+----------+---------
+ col_keep | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (1000)
+Indexes:
+    "idxpart1_col_keep_idx" btree (col_keep)
+
+select attrelid::regclass, attname, attnum from pg_attribute
+  where attrelid::regclass::text like 'idxpart%' and attnum > 0
+  order by attrelid::regclass, attnum;
+       attrelid        |           attname            | attnum 
+-----------------------+------------------------------+--------
+ idxpart1              | ........pg.dropped.1........ |      1
+ idxpart1              | ........pg.dropped.2........ |      2
+ idxpart1              | col_keep                     |      3
+ idxpart1              | ........pg.dropped.4........ |      4
+ idxpart1_col_keep_idx | col_keep                     |      1
+ idxpart               | col_keep                     |      1
+ idxpart_col_keep_idx  | col_keep                     |      1
+(7 rows)
+
+drop table idxpart;
+-- Column number mapping: dropped columns in the parent table
+create table idxpart(drop_1 int, drop_2 int, col_keep int, drop_3 int) partition by range (col_keep);
+alter table idxpart drop column drop_1;
+alter table idxpart drop column drop_2;
+alter table idxpart drop column drop_3;
+create table idxpart1 (col_keep int);
+create index on idxpart1 (col_keep);
+create index on idxpart (col_keep);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart
+               Table "public.idxpart"
+  Column  |  Type   | Collation | Nullable | Default 
+----------+---------+-----------+----------+---------
+ col_keep | integer |           |          | 
+Partition key: RANGE (col_keep)
+Indexes:
+    "idxpart_col_keep_idx" btree (col_keep)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart1
+               Table "public.idxpart1"
+  Column  |  Type   | Collation | Nullable | Default 
+----------+---------+-----------+----------+---------
+ col_keep | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (1000)
+Indexes:
+    "idxpart1_col_keep_idx" btree (col_keep)
+
+select attrelid::regclass, attname, attnum from pg_attribute
+  where attrelid::regclass::text like 'idxpart%' and attnum > 0
+  order by attrelid::regclass, attnum;
+       attrelid        |           attname            | attnum 
+-----------------------+------------------------------+--------
+ idxpart               | ........pg.dropped.1........ |      1
+ idxpart               | ........pg.dropped.2........ |      2
+ idxpart               | col_keep                     |      3
+ idxpart               | ........pg.dropped.4........ |      4
+ idxpart1              | col_keep                     |      1
+ idxpart1_col_keep_idx | col_keep                     |      1
+ idxpart_col_keep_idx  | col_keep                     |      1
+(7 rows)
+
+drop table idxpart;
+-- intentionally leave some objects around
+create table idxpart (a int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (100);
+create table idxpart2 partition of idxpart for values from (100) to (1000)
+  partition by range (a);
+create table idxpart21 partition of idxpart2 for values from (100) to (200);
+create table idxpart22 partition of idxpart2 for values from (200) to (300);
+create index on idxpart22 (a);
+create index on only idxpart2 (a);
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+create index on idxpart (a);
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e224977791e1225d85968ce74dbdf4428544a684..ad9434fb87d20b75869bb1649320774965ebc554 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,7 +116,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
 # ----------
 # Another group of parallel tests
 # ----------
-test: identity partition_join partition_prune reloptions hash_part
+test: identity partition_join partition_prune reloptions hash_part indexing
 
 # event triggers cannot run concurrently with any test that runs DDL
 test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9fc5f1a268818ee43b7851a9684a14d0ca12213b..27cd49845e4bff475b9a8d1322f39b6904e3730d 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -184,5 +184,6 @@ test: partition_join
 test: partition_prune
 test: reloptions
 test: hash_part
+test: indexing
 test: event_trigger
 test: stats
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 02a33ca7c4005f4d010c4c2b4bb23ed07a023a79..af25ee9e771c40cc8c7d7c9b90ad2eb7ec4e5d70 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1330,6 +1330,22 @@ create table tab1 (a int, b text);
 create table tab2 (x int, y tab1);
 alter table tab1 alter column b type varchar; -- fails
 
+-- Alter column type that's part of a partitioned index
+create table at_partitioned (a int, b text) partition by range (a);
+create table at_part_1 partition of at_partitioned for values from (0) to (1000);
+insert into at_partitioned values (512, '0.123');
+create table at_part_2 (b text, a int);
+insert into at_part_2 values ('1.234', 1024);
+create index on at_partitioned (b);
+create index on at_partitioned (a);
+\d at_part_1
+\d at_part_2
+alter table at_partitioned attach partition at_part_2 for values from (1000) to (2000);
+\d at_part_2
+alter table at_partitioned alter column b type numeric using b::numeric;
+\d at_part_1
+\d at_part_2
+
 -- disallow recursive containment of row types
 create temp table recur1 (f1 int);
 alter table recur1 add column f2 recur1; -- fails
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
new file mode 100644
index 0000000000000000000000000000000000000000..33be718699c1143dd72a9933ed21d1a13af70287
--- /dev/null
+++ b/src/test/regress/sql/indexing.sql
@@ -0,0 +1,388 @@
+-- Creating an index on a partitioned table makes the partitions
+-- automatically get the index
+create table idxpart (a int, b int, c text) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+create table idxpart2 partition of idxpart for values from (10) to (100)
+	partition by range (b);
+create table idxpart21 partition of idxpart2 for values from (0) to (100);
+create index on idxpart (a);
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- Some unsupported features
+create table idxpart (a int, b int, c text) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+create unique index on idxpart (a);
+create index concurrently on idxpart (a);
+drop table idxpart;
+
+-- If a table without index is attached as partition to a table with
+-- an index, the index is automatically created
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (b, c);
+create table idxpart1 (like idxpart);
+\d idxpart1
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+\d idxpart1
+drop table idxpart;
+
+-- If a partition already has an index, don't create a duplicative one
+create table idxpart (a int, b int) partition by range (a, b);
+create table idxpart1 partition of idxpart for values from (0, 0) to (10, 10);
+create index on idxpart1 (a, b);
+create index on idxpart (a, b);
+\d idxpart1
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- DROP behavior for partitioned indexes
+create table idxpart (a int) partition by range (a);
+create index on idxpart (a);
+create table idxpart1 partition of idxpart for values from (0) to (10);
+drop index idxpart1_a_idx;	-- no way
+drop index idxpart_a_idx;	-- both indexes go away
+select relname, relkind from pg_class
+  where relname like 'idxpart%' order by relname;
+create index on idxpart (a);
+drop table idxpart1;		-- the index on partition goes away too
+select relname, relkind from pg_class
+  where relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- ALTER INDEX .. ATTACH, error cases
+create table idxpart (a int, b int) partition by range (a, b);
+create table idxpart1 partition of idxpart for values from (0, 0) to (10, 10);
+create index idxpart_a_b_idx on only idxpart (a, b);
+create index idxpart1_a_b_idx on idxpart1 (a, b);
+create index idxpart1_tst1 on idxpart1 (b, a);
+create index idxpart1_tst2 on idxpart1 using hash (a);
+create index idxpart1_tst3 on idxpart1 (a, b) where a > 10;
+
+alter index idxpart attach partition idxpart1;
+alter index idxpart_a_b_idx attach partition idxpart1;
+alter index idxpart_a_b_idx attach partition idxpart_a_b_idx;
+alter index idxpart_a_b_idx attach partition idxpart1_b_idx;
+alter index idxpart_a_b_idx attach partition idxpart1_tst1;
+alter index idxpart_a_b_idx attach partition idxpart1_tst2;
+alter index idxpart_a_b_idx attach partition idxpart1_tst3;
+-- OK
+alter index idxpart_a_b_idx attach partition idxpart1_a_b_idx;
+alter index idxpart_a_b_idx attach partition idxpart1_a_b_idx; -- quiet
+
+-- reject dupe
+create index idxpart1_2_a_b on idxpart1 (a, b);
+alter index idxpart_a_b_idx attach partition idxpart1_2_a_b;
+drop table idxpart;
+-- make sure everything's gone
+select indexrelid::regclass, indrelid::regclass
+  from pg_index where indexrelid::regclass::text like 'idxpart%';
+
+-- Don't auto-attach incompatible indexes
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 (a int, b int);
+create index on idxpart1 using hash (a);
+create index on idxpart1 (a) where b > 1;
+create index on idxpart1 ((a + 0));
+create index on idxpart1 (a, a);
+create index on idxpart (a);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart1
+drop table idxpart;
+
+-- If CREATE INDEX ONLY, don't create indexes on partitions; and existing
+-- indexes on partitions don't change parent.  ALTER INDEX ATTACH can change
+-- the parent after the fact.
+create table idxpart (a int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (100);
+create table idxpart2 partition of idxpart for values from (100) to (1000)
+  partition by range (a);
+create table idxpart21 partition of idxpart2 for values from (100) to (200);
+create table idxpart22 partition of idxpart2 for values from (200) to (300);
+create index on idxpart22 (a);
+create index on only idxpart2 (a);
+create index on idxpart (a);
+-- Here we expect that idxpart1 and idxpart2 have a new index, but idxpart21
+-- does not; also, idxpart22 is not attached.
+\d idxpart1
+\d idxpart2
+\d idxpart21
+select indexrelid::regclass, indrelid::regclass, inhparent::regclass
+  from pg_index idx left join pg_inherits inh on (idx.indexrelid = inh.inhrelid)
+where indexrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+select indexrelid::regclass, indrelid::regclass, inhparent::regclass
+  from pg_index idx left join pg_inherits inh on (idx.indexrelid = inh.inhrelid)
+where indexrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+-- attaching idxpart22 is not enough to set idxpart22_a_idx valid ...
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+\d idxpart2
+-- ... but this one is.
+create index on idxpart21 (a);
+alter index idxpart2_a_idx attach partition idxpart21_a_idx;
+\d idxpart2
+drop table idxpart;
+
+-- When a table is attached a partition and it already has an index, a
+-- duplicate index should not get created, but rather the index becomes
+-- attached to the parent's index.
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (b, c);
+create table idxpart1 (like idxpart including indexes);
+\d idxpart1
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+\d idxpart1
+select relname, relkind, inhparent::regclass
+    from pg_class left join pg_index ix on (indexrelid = oid)
+	left join pg_inherits on (ix.indexrelid = inhrelid)
+	where relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- Verify that attaching an invalid index does not mark the parent index valid.
+-- On the other hand, attaching a valid index marks not only its direct
+-- ancestor valid, but also any indirect ancestor that was only missing the one
+-- that was just made valid
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (1) to (1000) partition by range (a);
+create table idxpart11 partition of idxpart1 for values from (1) to (100);
+create index on only idxpart1 (a);
+create index on only idxpart (a);
+-- this results in two invalid indexes:
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+-- idxpart1_a_idx is not valid, so idxpart_a_idx should not become valid:
+alter index idxpart_a_idx attach partition idxpart1_a_idx;
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+-- after creating and attaching this, both idxpart1_a_idx and idxpart_a_idx
+-- should become valid
+create index on idxpart11 (a);
+alter index idxpart1_a_idx attach partition idxpart11_a_idx;
+select relname, indisvalid from pg_class join pg_index on indexrelid = oid
+   where relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- verify dependency handling during ALTER TABLE DETACH PARTITION
+create table idxpart (a int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 (a);
+create index on idxpart (a);
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+-- a) after detaching partitions, the indexes can be dropped independently
+alter table idxpart detach partition idxpart1;
+alter table idxpart detach partition idxpart2;
+alter table idxpart detach partition idxpart3;
+drop index idxpart1_a_idx;
+drop index idxpart2_a_idx;
+drop index idxpart3_a_idx;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+drop table idxpart, idxpart1, idxpart2, idxpart3;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+
+create table idxpart (a int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 (a);
+create index on idxpart (a);
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+-- b) after detaching, dropping the index on parent does not remove the others
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+alter table idxpart detach partition idxpart1;
+alter table idxpart detach partition idxpart2;
+alter table idxpart detach partition idxpart3;
+drop index idxpart_a_idx;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+drop table idxpart, idxpart1, idxpart2, idxpart3;
+select relname, relkind from pg_class where relname like 'idxpart%' order by relname;
+
+-- Verify that expression indexes inherit correctly
+create table idxpart (a int, b int) partition by range (a);
+create table idxpart1 (like idxpart);
+create index on idxpart1 ((a + b));
+create index on idxpart ((a + b));
+create table idxpart2 (like idxpart);
+alter table idxpart attach partition idxpart1 for values from (0000) to (1000);
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create table idxpart3 partition of idxpart for values from (2000) to (3000);
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- Verify behavior for collation (mis)matches
+create table idxpart (a text) partition by range (a);
+create table idxpart1 (like idxpart);
+create table idxpart2 (like idxpart);
+create index on idxpart2 (a collate "POSIX");
+create index on idxpart2 (a);
+create index on idxpart2 (a collate "C");
+alter table idxpart attach partition idxpart1 for values from ('aaa') to ('bbb');
+alter table idxpart attach partition idxpart2 for values from ('bbb') to ('ccc');
+create table idxpart3 partition of idxpart for values from ('ccc') to ('ddd');
+create index on idxpart (a collate "C");
+create table idxpart4 partition of idxpart for values from ('ddd') to ('eee');
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- Verify behavior for opclass (mis)matches
+create table idxpart (a text) partition by range (a);
+create table idxpart1 (like idxpart);
+create table idxpart2 (like idxpart);
+create index on idxpart2 (a);
+alter table idxpart attach partition idxpart1 for values from ('aaa') to ('bbb');
+alter table idxpart attach partition idxpart2 for values from ('bbb') to ('ccc');
+create table idxpart3 partition of idxpart for values from ('ccc') to ('ddd');
+create index on idxpart (a text_pattern_ops);
+create table idxpart4 partition of idxpart for values from ('ddd') to ('eee');
+-- must *not* have attached the index we created on idxpart2
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+drop index idxpart_a_idx;
+create index on only idxpart (a text_pattern_ops);
+-- must reject
+alter index idxpart_a_idx attach partition idxpart2_a_idx;
+drop table idxpart;
+
+-- Verify that attaching indexes maps attribute numbers correctly
+create table idxpart (col1 int, a int, col2 int, b int) partition by range (a);
+create table idxpart1 (b int, col1 int, col2 int, col3 int, a int);
+alter table idxpart drop column col1, drop column col2;
+alter table idxpart1 drop column col1, drop column col2, drop column col3;
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+create index idxpart_1_idx on only idxpart (b, a);
+create index idxpart1_1_idx on idxpart1 (b, a);
+create index idxpart1_1b_idx on idxpart1 (b);
+-- test expressions and partial-index predicate, too
+create index idxpart_2_idx on only idxpart ((b + a)) where a > 1;
+create index idxpart1_2_idx on idxpart1 ((b + a)) where a > 1;
+create index idxpart1_2b_idx on idxpart1 ((a + b)) where a > 1;
+create index idxpart1_2c_idx on idxpart1 ((b + a)) where b > 1;
+alter index idxpart_1_idx attach partition idxpart1_1b_idx;	-- fail
+alter index idxpart_1_idx attach partition idxpart1_1_idx;
+alter index idxpart_2_idx attach partition idxpart1_2b_idx;	-- fail
+alter index idxpart_2_idx attach partition idxpart1_2c_idx;	-- fail
+alter index idxpart_2_idx attach partition idxpart1_2_idx;	-- ok
+select relname as child, inhparent::regclass as parent, pg_get_indexdef as childdef
+  from pg_class left join pg_inherits on inhrelid = oid,
+  lateral pg_get_indexdef(pg_class.oid)
+  where relkind in ('i', 'I') and relname like 'idxpart%' order by relname;
+drop table idxpart;
+
+-- Make sure the partition columns are mapped correctly
+create table idxpart (a int, b int, c text) partition by range (a);
+create index idxparti on idxpart (a);
+create index idxparti2 on idxpart (c, b);
+create table idxpart1 (c text, a int, b int);
+alter table idxpart attach partition idxpart1 for values from (0) to (10);
+create table idxpart2 (c text, a int, b int);
+create index on idxpart2 (a);
+create index on idxpart2 (c, b);
+alter table idxpart attach partition idxpart2 for values from (10) to (20);
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+drop table idxpart;
+
+-- Verify that columns are mapped correctly in expression indexes
+create table idxpart (col1 int, col2 int, a int, b int) partition by range (a);
+create table idxpart1 (col2 int, b int, col1 int, a int);
+create table idxpart2 (col1 int, col2 int, b int, a int);
+alter table idxpart drop column col1, drop column col2;
+alter table idxpart1 drop column col1, drop column col2;
+alter table idxpart2 drop column col1, drop column col2;
+create index on idxpart2 (abs(b));
+alter table idxpart attach partition idxpart2 for values from (0) to (1);
+create index on idxpart (abs(b));
+alter table idxpart attach partition idxpart1 for values from (1) to (2);
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+drop table idxpart;
+
+-- Verify that columns are mapped correctly for WHERE in a partial index
+create table idxpart (col1 int, a int, col3 int, b int) partition by range (a);
+alter table idxpart drop column col1, drop column col3;
+create table idxpart1 (col1 int, col2 int, col3 int, col4 int, b int, a int);
+alter table idxpart1 drop column col1, drop column col2, drop column col3, drop column col4;
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+create table idxpart2 (col1 int, col2 int, b int, a int);
+create index on idxpart2 (a) where b > 1000;
+alter table idxpart2 drop column col1, drop column col2;
+alter table idxpart attach partition idxpart2 for values from (1000) to (2000);
+create index on idxpart (a) where b > 1000;
+select c.relname, pg_get_indexdef(indexrelid)
+  from pg_class c join pg_index i on c.oid = i.indexrelid
+  where indrelid::regclass::text like 'idxpart%'
+  order by indrelid::regclass::text collate "C";
+drop table idxpart;
+
+-- Column number mapping: dropped columns in the partition
+create table idxpart1 (drop_1 int, drop_2 int, col_keep int, drop_3 int);
+alter table idxpart1 drop column drop_1;
+alter table idxpart1 drop column drop_2;
+alter table idxpart1 drop column drop_3;
+create index on idxpart1 (col_keep);
+create table idxpart (col_keep int) partition by range (col_keep);
+create index on idxpart (col_keep);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart
+\d idxpart1
+select attrelid::regclass, attname, attnum from pg_attribute
+  where attrelid::regclass::text like 'idxpart%' and attnum > 0
+  order by attrelid::regclass, attnum;
+drop table idxpart;
+
+-- Column number mapping: dropped columns in the parent table
+create table idxpart(drop_1 int, drop_2 int, col_keep int, drop_3 int) partition by range (col_keep);
+alter table idxpart drop column drop_1;
+alter table idxpart drop column drop_2;
+alter table idxpart drop column drop_3;
+create table idxpart1 (col_keep int);
+create index on idxpart1 (col_keep);
+create index on idxpart (col_keep);
+alter table idxpart attach partition idxpart1 for values from (0) to (1000);
+\d idxpart
+\d idxpart1
+select attrelid::regclass, attname, attnum from pg_attribute
+  where attrelid::regclass::text like 'idxpart%' and attnum > 0
+  order by attrelid::regclass, attnum;
+drop table idxpart;
+
+-- intentionally leave some objects around
+create table idxpart (a int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (0) to (100);
+create table idxpart2 partition of idxpart for values from (100) to (1000)
+  partition by range (a);
+create table idxpart21 partition of idxpart2 for values from (100) to (200);
+create table idxpart22 partition of idxpart2 for values from (200) to (300);
+create index on idxpart22 (a);
+create index on only idxpart2 (a);
+alter index idxpart2_a_idx attach partition idxpart22_a_idx;
+create index on idxpart (a);