From 688b801b8799ecd9827ee203bfb690536ac84ff4 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Mon, 4 Feb 2019 13:43:58 -0300
Subject: [PATCH v2 2/2] Propagate replica identity to partitions

---
 src/backend/commands/tablecmds.c               | 108 +++++++++++++++--
 src/bin/psql/describe.c                        |   3 +-
 src/test/regress/expected/replica_identity.out | 160 +++++++++++++++++++++++++
 src/test/regress/sql/replica_identity.sql      |  43 +++++++
 4 files changed, 304 insertions(+), 10 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 877bac506f..22cec85ab0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -300,7 +300,7 @@ static void truncate_check_activity(Relation rel);
 static void RangeVarCallbackForTruncate(const RangeVar *relation,
 							Oid relId, Oid oldRelId, void *arg);
 static List *MergeAttributes(List *schema, List *supers, char relpersistence,
-				bool is_partition, List **supconstr);
+				bool is_partition, List **supconstr, char *ri_type);
 static bool MergeCheckConstraint(List *constraints, char *name, Node *expr);
 static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel);
 static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
@@ -485,6 +485,7 @@ static void AttachPartitionEnsureIndexes(Relation rel, Relation attachrel);
 static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
 								   List *partConstraint,
 								   bool validate_default);
+static void MatchReplicaIdentity(Relation tgtrel, Relation srcrel);
 static void CloneRowTriggersToPartition(Relation parent, Relation partition);
 static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
 static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
@@ -527,6 +528,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	TupleDesc	descriptor;
 	List	   *inheritOids;
 	List	   *old_constraints;
+	char		ri_type;
 	List	   *rawDefaults;
 	List	   *cookedDefaults;
 	Datum		reloptions;
@@ -708,7 +710,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		MergeAttributes(stmt->tableElts, inheritOids,
 						stmt->relation->relpersistence,
 						stmt->partbound != NULL,
-						&old_constraints);
+						&old_constraints, &ri_type);
 
 	/*
 	 * Create a tuple descriptor from the relation schema.  Note that this
@@ -1014,6 +1016,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		 */
 		CloneForeignKeyConstraints(parentId, relationId, NULL);
 
+		/* Propagate REPLICA IDENTITY information too */
+		if (ri_type != REPLICA_IDENTITY_DEFAULT)
+			MatchReplicaIdentity(rel, parent);
+
 		table_close(parent, NoLock);
 	}
 
@@ -1873,6 +1879,8 @@ storage_name(char c)
  * Output arguments:
  * 'supconstr' receives a list of constraints belonging to the parents,
  *		updated as necessary to be valid for the child.
+ * 'ri_type' receives the replica identity type of the last parent seen,
+ *		or default if none.
  *
  * Return value:
  * Completed schema list.
@@ -1914,11 +1922,15 @@ storage_name(char c)
  *		(4) Otherwise the inherited default is used.
  *		Rule (3) is new in Postgres 7.1; in earlier releases you got a
  *		rather arbitrary choice of which parent default to use.
+ *
+ *	  It only makes sense to use the returned 'ri_type' when there's a single
+ *	  parent, such as in declarative partitioning.
  *----------
  */
 static List *
 MergeAttributes(List *schema, List *supers, char relpersistence,
-				bool is_partition, List **supconstr)
+				bool is_partition, List **supconstr,
+				char *ri_type)
 {
 	ListCell   *entry;
 	List	   *inhSchema = NIL;
@@ -2015,6 +2027,11 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 	}
 
 	/*
+	 * Initialize replica identity to default; parents may change it later
+	 */
+	*ri_type = REPLICA_IDENTITY_DEFAULT;
+
+	/*
 	 * Scan the parents left-to-right, and merge their attributes to form a
 	 * list of inherited attributes (inhSchema).  Also check to see if we need
 	 * to inherit an OID column.
@@ -2095,6 +2112,9 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 							? "cannot inherit from temporary relation of another session"
 							: "cannot create as partition of temporary relation of another session")));
 
+		/* Indicate replica identity back to caller */
+		*ri_type = relation->rd_rel->relreplident;
+
 		/*
 		 * We should have an UNDER permission flag for this, but for now,
 		 * demand that creator of a child table own the parent.
@@ -3935,7 +3955,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_ReplicaIdentity:	/* REPLICA IDENTITY ... */
 			ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW);
 			pass = AT_PASS_MISC;
-			/* This command never recurses */
+			/* Recursion occurs during execution phase */
 			/* No command-specific prep needed */
 			break;
 		case AT_EnableTrig:		/* ENABLE TRIGGER variants */
@@ -12756,7 +12776,7 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
  */
 static void
 relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
-							   bool is_internal)
+							   bool is_internal, LOCKMODE lockmode)
 {
 	Relation	pg_index;
 	Relation	pg_class;
@@ -12847,6 +12867,42 @@ relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
 	}
 
 	table_close(pg_index, RowExclusiveLock);
+
+	/*
+	 * If there are any partitions, handle them too.
+	 */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		PartitionDesc	pd = RelationGetPartitionDesc(rel);
+
+		for (int i = 0; i < pd->nparts; i++)
+		{
+			Relation	part = table_open(pd->oids[i], lockmode);
+			Oid			idxOid;
+
+			if (ri_type == REPLICA_IDENTITY_INDEX)
+			{
+				idxOid = index_get_partition(part, indexOid);
+				if (!OidIsValid(idxOid))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							 errmsg("replica index does not exist in partition \"%s\"",
+									RelationGetRelationName(part))));
+
+				LockRelationOid(idxOid, ShareLock);
+			}
+			else
+				idxOid = InvalidOid;
+
+			idxOid = ri_type == REPLICA_IDENTITY_INDEX ?
+				index_get_partition(part, indexOid) : InvalidOid;
+
+			relation_mark_replica_identity(part, ri_type, idxOid, true,
+										   lockmode);
+
+			table_close(part, NoLock);
+		}
+	}
 }
 
 /*
@@ -12861,17 +12917,20 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 
 	if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
 	{
-		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
+		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true,
+									   lockmode);
 		return;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_FULL)
 	{
-		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
+		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true,
+									   lockmode);
 		return;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_NOTHING)
 	{
-		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
+		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true,
+									   lockmode);
 		return;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_INDEX)
@@ -12959,7 +13018,8 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	}
 
 	/* This index is suitable for use as a replica identity. Mark it. */
-	relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true);
+	relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true,
+								   lockmode);
 
 	index_close(indexRel, NoLock);
 }
@@ -14664,6 +14724,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
 	/* and triggers */
 	CloneRowTriggersToPartition(rel, attachrel);
 
+	/* Propagate REPLICA IDENTITY information */
+	if (rel->rd_rel->relreplident != REPLICA_IDENTITY_DEFAULT)
+		MatchReplicaIdentity(attachrel, rel);
+
 	/*
 	 * Clone foreign key constraints, and setup for Phase 3 to verify them.
 	 */
@@ -14915,6 +14979,32 @@ AttachPartitionEnsureIndexes(Relation rel, Relation attachrel)
 }
 
 /*
+ * Set up partRel's (a partition) replica identity to match parentRel's (its
+ * parent).
+ */
+static void
+MatchReplicaIdentity(Relation partRel, Relation srcrel)
+{
+	Oid		ri_index;
+
+	if (srcrel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX)
+	{
+		ri_index = index_get_partition(partRel,
+									   RelationGetReplicaIndex(srcrel));
+		if (!OidIsValid(ri_index))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("replica index does not exist in partition \"%s\"",
+							RelationGetRelationName(partRel))));
+	}
+	else
+		ri_index = InvalidOid;
+
+	relation_mark_replica_identity(partRel, srcrel->rd_rel->relreplident,
+								   ri_index, true, AccessExclusiveLock);
+}
+
+/*
  * CloneRowTriggersToPartition
  *		subroutine for ATExecAttachPartition/DefineRelation to create row
  *		triggers on partitions
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4da6719ce7..6145a000cb 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3113,7 +3113,8 @@ describeOneTableDetails(const char *schemaname,
 
 		if (verbose &&
 			(tableinfo.relkind == RELKIND_RELATION ||
-			 tableinfo.relkind == RELKIND_MATVIEW) &&
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE) &&
 
 		/*
 		 * No need to display default values; we already display a REPLICA
diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out
index 175ecd2879..d6014df840 100644
--- a/src/test/regress/expected/replica_identity.out
+++ b/src/test/regress/expected/replica_identity.out
@@ -181,3 +181,163 @@ SELECT relreplident FROM pg_class WHERE oid = 'test_replica_identity'::regclass;
 
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity_othertable;
+----
+-- Make sure it propagates to partitions
+----
+CREATE TABLE test_replica_identity_part (a int, b int) PARTITION BY RANGE (a);
+CREATE TABLE test_replica_identity_part1 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (0) TO (1000) PARTITION BY RANGE (a);
+CREATE TABLE test_replica_identity_part2 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (1000) TO (2000);
+CREATE TABLE test_replica_identity_part11 PARTITION OF test_replica_identity_part1
+  FOR VALUES FROM (1000) TO (1500);
+ALTER TABLE test_replica_identity_part REPLICA IDENTITY FULL;
+CREATE TABLE test_replica_identity_part3 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (2000) TO (3000);
+CREATE TABLE test_replica_identity_part4 (LIKE test_replica_identity_part);
+ALTER TABLE test_replica_identity_part ATTACH PARTITION test_replica_identity_part4
+  FOR VALUES FROM (3000) TO (4000);
+\d+ test_replica_identity_part2
+                        Table "public.test_replica_identity_part2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (1000) TO (2000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1000) AND (a < 2000))
+Replica Identity: FULL
+
+\d+ test_replica_identity_part11
+                       Table "public.test_replica_identity_part11"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part1 FOR VALUES FROM (1000) TO (1500)
+Partition constraint: ((a IS NOT NULL) AND (a >= 0) AND (a < 1000) AND (a IS NOT NULL) AND (a >= 1000) AND (a < 1500))
+Replica Identity: FULL
+
+\d+ test_replica_identity_part
+                  Partitioned table "public.test_replica_identity_part"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition key: RANGE (a)
+Partitions: test_replica_identity_part1 FOR VALUES FROM (0) TO (1000), PARTITIONED,
+            test_replica_identity_part2 FOR VALUES FROM (1000) TO (2000),
+            test_replica_identity_part3 FOR VALUES FROM (2000) TO (3000),
+            test_replica_identity_part4 FOR VALUES FROM (3000) TO (4000)
+Replica Identity: FULL
+
+\d+ test_replica_identity_part3
+                        Table "public.test_replica_identity_part3"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (2000) TO (3000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 2000) AND (a < 3000))
+Replica Identity: FULL
+
+\d+ test_replica_identity_part4
+                        Table "public.test_replica_identity_part4"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (3000) TO (4000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 3000) AND (a < 4000))
+Replica Identity: FULL
+
+ALTER TABLE test_replica_identity_part ALTER a SET NOT NULL;
+CREATE UNIQUE INDEX trip_b_idx ON test_replica_identity_part (a);
+ALTER TABLE test_replica_identity_part REPLICA IDENTITY USING INDEX trip_b_idx;
+\d+ test_replica_identity_part2
+                        Table "public.test_replica_identity_part2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (1000) TO (2000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1000) AND (a < 2000))
+Indexes:
+    "test_replica_identity_part2_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+
+\d+ test_replica_identity_part11
+                       Table "public.test_replica_identity_part11"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part1 FOR VALUES FROM (1000) TO (1500)
+Partition constraint: ((a IS NOT NULL) AND (a >= 0) AND (a < 1000) AND (a IS NOT NULL) AND (a >= 1000) AND (a < 1500))
+Indexes:
+    "test_replica_identity_part11_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+
+\d+ test_replica_identity_part
+                  Partitioned table "public.test_replica_identity_part"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition key: RANGE (a)
+Indexes:
+    "trip_b_idx" UNIQUE, btree (a) REPLICA IDENTITY
+Partitions: test_replica_identity_part1 FOR VALUES FROM (0) TO (1000), PARTITIONED,
+            test_replica_identity_part2 FOR VALUES FROM (1000) TO (2000),
+            test_replica_identity_part3 FOR VALUES FROM (2000) TO (3000),
+            test_replica_identity_part4 FOR VALUES FROM (3000) TO (4000)
+
+\d+ test_replica_identity_part3
+                        Table "public.test_replica_identity_part3"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (2000) TO (3000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 2000) AND (a < 3000))
+Indexes:
+    "test_replica_identity_part3_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+
+\d+ test_replica_identity_part4
+                        Table "public.test_replica_identity_part4"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Partition of: test_replica_identity_part FOR VALUES FROM (3000) TO (4000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 3000) AND (a < 4000))
+Indexes:
+    "test_replica_identity_part4_a_idx" UNIQUE, btree (a) REPLICA IDENTITY
+
+----
+-- Check behavior with inherited tables
+----
+CREATE TABLE test_replica_identity_inh (a int);
+CREATE TABLE test_replica_identity_cld () INHERITS (test_replica_identity_inh);
+ALTER TABLE test_replica_identity_inh REPLICA IDENTITY FULL;
+CREATE TABLE test_replica_identity_cld2 () INHERITS (test_replica_identity_inh);
+\d+ test_replica_identity_inh
+                         Table "public.test_replica_identity_inh"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Child tables: test_replica_identity_cld,
+              test_replica_identity_cld2
+Replica Identity: FULL
+
+\d+ test_replica_identity_cld
+                         Table "public.test_replica_identity_cld"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: test_replica_identity_inh
+
+\d+ test_replica_identity_cld2
+                        Table "public.test_replica_identity_cld2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: test_replica_identity_inh
+
diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql
index b08a3623b8..9e309796f2 100644
--- a/src/test/regress/sql/replica_identity.sql
+++ b/src/test/regress/sql/replica_identity.sql
@@ -77,3 +77,46 @@ SELECT relreplident FROM pg_class WHERE oid = 'test_replica_identity'::regclass;
 
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity_othertable;
+
+----
+-- Make sure it propagates to partitions
+----
+CREATE TABLE test_replica_identity_part (a int, b int) PARTITION BY RANGE (a);
+CREATE TABLE test_replica_identity_part1 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (0) TO (1000) PARTITION BY RANGE (a);
+CREATE TABLE test_replica_identity_part2 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (1000) TO (2000);
+CREATE TABLE test_replica_identity_part11 PARTITION OF test_replica_identity_part1
+  FOR VALUES FROM (1000) TO (1500);
+ALTER TABLE test_replica_identity_part REPLICA IDENTITY FULL;
+CREATE TABLE test_replica_identity_part3 PARTITION OF test_replica_identity_part
+  FOR VALUES FROM (2000) TO (3000);
+CREATE TABLE test_replica_identity_part4 (LIKE test_replica_identity_part);
+ALTER TABLE test_replica_identity_part ATTACH PARTITION test_replica_identity_part4
+  FOR VALUES FROM (3000) TO (4000);
+\d+ test_replica_identity_part2
+\d+ test_replica_identity_part11
+\d+ test_replica_identity_part
+\d+ test_replica_identity_part3
+\d+ test_replica_identity_part4
+
+ALTER TABLE test_replica_identity_part ALTER a SET NOT NULL;
+CREATE UNIQUE INDEX trip_b_idx ON test_replica_identity_part (a);
+ALTER TABLE test_replica_identity_part REPLICA IDENTITY USING INDEX trip_b_idx;
+\d+ test_replica_identity_part2
+\d+ test_replica_identity_part11
+\d+ test_replica_identity_part
+\d+ test_replica_identity_part3
+\d+ test_replica_identity_part4
+
+
+----
+-- Check behavior with inherited tables
+----
+CREATE TABLE test_replica_identity_inh (a int);
+CREATE TABLE test_replica_identity_cld () INHERITS (test_replica_identity_inh);
+ALTER TABLE test_replica_identity_inh REPLICA IDENTITY FULL;
+CREATE TABLE test_replica_identity_cld2 () INHERITS (test_replica_identity_inh);
+\d+ test_replica_identity_inh
+\d+ test_replica_identity_cld
+\d+ test_replica_identity_cld2
-- 
2.11.0

