v14-0003-Add-shared-index-for-conflict-log-table-lookup.patch

text/x-patch

Filename: v14-0003-Add-shared-index-for-conflict-log-table-lookup.patch
Type: text/x-patch
Part: 2
Message: Re: Proposal: Conflict log history table for Logical Replication
From db4d988b4c8554be54d6f807208e4353632e8b2b Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Sun, 21 Dec 2025 19:46:01 +0530
Subject: [PATCH v14 3/3] Add shared index for conflict log table lookup

Introduce a dedicated shared unique index on
pg_subscription.subconflictlogrelid to make conflict log table
detection efficient, and index-backed.  Previously, IsConflictLogTable()
relied on a full catalog scan of pg_subscription, which was inefficient.
This change adds pg_subscription_conflictrel_index and marks it as a
shared index, matching the shared pg_subscription table, and rewrites
conflict log table detection to use an indexed systable scan.
---
 src/backend/catalog/catalog.c               |  1 +
 src/backend/catalog/pg_publication.c        | 12 +----------
 src/backend/catalog/pg_subscription.c       | 23 +++++++++++++++++++++
 src/backend/commands/subscriptioncmds.c     | 20 +++++++++++-------
 src/backend/replication/logical/conflict.c  |  4 +---
 src/backend/replication/pgoutput/pgoutput.c | 15 +++++++++++---
 src/bin/psql/describe.c                     |  4 +++-
 src/include/catalog/pg_proc.dat             |  7 +++++++
 src/include/catalog/pg_subscription.h       |  1 +
 src/test/regress/expected/subscription.out  |  7 ++++---
 src/test/regress/sql/subscription.sql       |  5 +++--
 11 files changed, 69 insertions(+), 30 deletions(-)

diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 59caae8f1bc..b95a0a36cd0 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -336,6 +336,7 @@ IsSharedRelation(Oid relationId)
 		relationId == SharedSecLabelObjectIndexId ||
 		relationId == SubscriptionNameIndexId ||
 		relationId == SubscriptionObjectIndexId ||
+		relationId == SubscriptionConflictrelIndexId ||
 		relationId == TablespaceNameIndexId ||
 		relationId == TablespaceOidIndexId)
 		return true;
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9f84e02b7ef..187eb351f3f 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -86,15 +86,6 @@ check_publication_add_relation(Relation targetrel)
 				 errmsg("cannot add relation \"%s\" to publication",
 						RelationGetRelationName(targetrel)),
 				 errdetail("This operation is not supported for unlogged tables.")));
-
-	/* Can't be conflict log table */
-	if (IsConflictLogTable(RelationGetRelid(targetrel)))
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("cannot add relation \"%s.%s\" to publication",
-						get_namespace_name(RelationGetNamespace(targetrel)),
-						RelationGetRelationName(targetrel)),
-				 errdetail("This operation is not supported for conflict log tables.")));
 }
 
 /*
@@ -188,8 +179,7 @@ pg_relation_is_publishable(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 
 	/* Subscription conflict log tables are not published */
-	result = is_publishable_class(relid, (Form_pg_class) GETSTRUCT(tuple)) &&
-			 !IsConflictLogTable(relid);
+	result = is_publishable_class(relid, (Form_pg_class) GETSTRUCT(tuple));
 	ReleaseSysCache(tuple);
 	PG_RETURN_BOOL(result);
 }
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 27a9aee1c56..dae44c659f8 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -22,6 +22,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
+#include "commands/subscriptioncmds.h"
 #include "miscadmin.h"
 #include "storage/lmgr.h"
 #include "utils/array.h"
@@ -156,6 +157,28 @@ GetSubscription(Oid subid, bool missing_ok)
 	return sub;
 }
 
+/*
+ * pg_relation_is_conflict_log_table
+ *
+ * Returns true if the given relation OID is used as a conflict log table
+ * by any subscription, else returns false.
+ */
+Datum
+pg_relation_is_conflict_log_table(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	HeapTuple	tuple;
+	bool		result;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		PG_RETURN_NULL();
+
+	result = IsConflictLogTable(relid);
+	ReleaseSysCache(tuple);
+	PG_RETURN_BOOL(result);
+}
+
 /*
  * Return number of subscriptions defined in given database.
  * Used by dropdb() to check if database can indeed be dropped.
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 65c4c0dd8e4..44821102833 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -51,6 +51,7 @@
 #include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -3490,27 +3491,32 @@ bool
 IsConflictLogTable(Oid relid)
 {
 	Relation        rel;
-	TableScanDesc   scan;
+	ScanKeyData 	scankey;
+	SysScanDesc		scan;
 	HeapTuple       tup;
 	bool            is_clt = false;
 
 	rel = table_open(SubscriptionRelationId, AccessShareLock);
-	scan = table_beginscan_catalog(rel, 0, NULL);
 
-	while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+	ScanKeyInit(&scankey,
+				Anum_pg_subscription_subconflictlogrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid));
+
+	scan = systable_beginscan(rel, SubscriptionConflictrelIndexId,
+							  true, NULL, 1, &scankey);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
 	{
 		Form_pg_subscription subform = (Form_pg_subscription) GETSTRUCT(tup);
 
-		/* Direct Oid comparison from catalog */
-		if (OidIsValid(subform->subconflictlogrelid) &&
-			subform->subconflictlogrelid == relid)
+		if (OidIsValid(subform->subconflictlogrelid))
 		{
 			is_clt = true;
 			break;
 		}
 	}
 
-	table_endscan(scan);
+	systable_endscan(scan);
 	table_close(rel, AccessShareLock);
 
 	return is_clt;
diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c
index 5f753cd8042..3aa319f043c 100644
--- a/src/backend/replication/logical/conflict.c
+++ b/src/backend/replication/logical/conflict.c
@@ -300,13 +300,11 @@ GetConflictLogTableInfo(ConflictLogDest *log_dest)
 void
 InsertConflictLogTuple(Relation conflictlogrel)
 {
-	int			options = HEAP_INSERT_NO_LOGICAL;
-
 	/* A valid tuple must be prepared and stored in MyLogicalRepWorker. */
 	Assert(MyLogicalRepWorker->conflict_log_tuple != NULL);
 
 	heap_insert(conflictlogrel, MyLogicalRepWorker->conflict_log_tuple,
-				GetCurrentCommandId(true), options, NULL);
+				GetCurrentCommandId(true), 0, NULL);
 
 	/* Free conflict log tuple. */
 	heap_freetuple(MyLogicalRepWorker->conflict_log_tuple);
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..fe37b7fc284 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -131,6 +131,8 @@ typedef struct RelationSyncEntry
 
 	bool		schema_sent;
 
+	bool		conflictlogrel; /* is this relation used for conflict logging? */
+
 	/*
 	 * This will be PUBLISH_GENCOLS_STORED if the relation contains generated
 	 * columns and the 'publish_generated_columns' parameter is set to
@@ -2067,6 +2069,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 	{
 		entry->replicate_valid = false;
 		entry->schema_sent = false;
+		entry->conflictlogrel = false;
 		entry->include_gencols_type = PUBLISH_GENCOLS_NONE;
 		entry->streamed_txns = NIL;
 		entry->pubactions.pubinsert = entry->pubactions.pubupdate =
@@ -2117,6 +2120,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 		 * earlier definition.
 		 */
 		entry->schema_sent = false;
+		entry->conflictlogrel = IsConflictLogTable(relid);
 		entry->include_gencols_type = PUBLISH_GENCOLS_NONE;
 		list_free(entry->streamed_txns);
 		entry->streamed_txns = NIL;
@@ -2199,7 +2203,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 			 * If this is a FOR ALL TABLES publication, pick the partition
 			 * root and set the ancestor level accordingly.
 			 */
-			if (pub->alltables)
+			if (pub->alltables && !entry->conflictlogrel)
 			{
 				publish = true;
 				if (pub->pubviaroot && am_partition)
@@ -2225,8 +2229,12 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				{
 					Oid			ancestor;
 					int			level;
-					List	   *ancestors = get_partition_ancestors(relid);
+					List	   *ancestors;
+
+					/* Conflict log table cannot be a partition */
+					Assert(entry->conflictlogrel == false);
 
+					ancestors = get_partition_ancestors(relid);
 					ancestor = GetTopMostAncestorInPublication(pub->oid,
 															   ancestors,
 															   &level);
@@ -2243,7 +2251,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				}
 
 				if (list_member_oid(pubids, pub->oid) ||
-					list_member_oid(schemaPubids, pub->oid) ||
+					(list_member_oid(schemaPubids, pub->oid) &&
+					 !entry->conflictlogrel) ||
 					ancestor_published)
 					publish = true;
 			}
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index cc80f0f661c..2d612448241 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3063,6 +3063,7 @@ describeOneTableDetails(const char *schemaname,
 								  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
 								  "     JOIN pg_catalog.pg_class pc ON pc.relnamespace = pn.pnnspid\n"
 								  "WHERE pc.oid ='%s' and pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "AND NOT pg_catalog.pg_relation_is_conflict_log_table('%s'::oid)\n"
 								  "UNION\n"
 								  "SELECT pubname\n"
 								  "     , pg_get_expr(pr.prqual, c.oid)\n"
@@ -3082,8 +3083,9 @@ describeOneTableDetails(const char *schemaname,
 								  "     , NULL\n"
 								  "FROM pg_catalog.pg_publication p\n"
 								  "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n"
+								  "AND NOT pg_catalog.pg_relation_is_conflict_log_table('%s'::oid)\n"
 								  "ORDER BY 1;",
-								  oid, oid, oid, oid);
+								  oid, oid, oid, oid, oid, oid);
 			}
 			else
 			{
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b9..98fe8eee012 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12321,6 +12321,13 @@
   prorettype => 'bool', proargtypes => 'regclass',
   prosrc => 'pg_relation_is_publishable' },
 
+# subscriptions
+{ oid => '6123',
+  descr => 'returns whether a relation is a subscription conflict log table',
+  proname => 'pg_relation_is_conflict_log_table', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'regclass',
+  prosrc => 'pg_relation_is_conflict_log_table' },
+
 # rls
 { oid => '3298',
   descr => 'row security for current context active on table by table oid',
diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h
index 55f4bfa0419..46c446eaf8b 100644
--- a/src/include/catalog/pg_subscription.h
+++ b/src/include/catalog/pg_subscription.h
@@ -123,6 +123,7 @@ DECLARE_TOAST_WITH_MACRO(pg_subscription, 4183, 4184, PgSubscriptionToastTable,
 
 DECLARE_UNIQUE_INDEX_PKEY(pg_subscription_oid_index, 6114, SubscriptionObjectIndexId, pg_subscription, btree(oid oid_ops));
 DECLARE_UNIQUE_INDEX(pg_subscription_subname_index, 6115, SubscriptionNameIndexId, pg_subscription, btree(subdbid oid_ops, subname name_ops));
+DECLARE_UNIQUE_INDEX(pg_subscription_conflictrel_index, 6122, SubscriptionConflictrelIndexId, pg_subscription, btree(oid oid_ops, subconflictlogrelid oid_ops));
 
 MAKE_SYSCACHE(SUBSCRIPTIONOID, pg_subscription_oid_index, 4);
 MAKE_SYSCACHE(SUBSCRIPTIONNAME, pg_subscription_subname_index, 4);
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index a678471f4c2..81d23a90830 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -635,13 +635,14 @@ EXCEPTION WHEN dependent_objects_still_exist THEN
     RAISE NOTICE 'captured expected error: dependent_objects_still_exist';
 END $$;
 NOTICE:  captured expected error: dependent_objects_still_exist
--- PUBLICATION: Verify internal tables are not publishable
--- pg_relation_is_publishable should return false for internal conflict log tables
+-- PUBLICATION: Verify internal tables are publishable
+-- pg_relation_is_publishable should return true for internal conflict log
+-- tables, as it can be published using TABLE publication.
 SELECT pg_relation_is_publishable(subconflictlogrelid)
 FROM pg_subscription WHERE subname = 'regress_conflict_test1';
  pg_relation_is_publishable 
 ----------------------------
- f
+ t
 (1 row)
 
 -- CLEANUP: Proper drop reaps the table
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index df0e4649007..d0debe25cb5 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -443,8 +443,9 @@ EXCEPTION WHEN dependent_objects_still_exist THEN
     RAISE NOTICE 'captured expected error: dependent_objects_still_exist';
 END $$;
 
--- PUBLICATION: Verify internal tables are not publishable
--- pg_relation_is_publishable should return false for internal conflict log tables
+-- PUBLICATION: Verify internal tables are publishable
+-- pg_relation_is_publishable should return true for internal conflict log
+-- tables, as it can be published using TABLE publication.
 SELECT pg_relation_is_publishable(subconflictlogrelid)
 FROM pg_subscription WHERE subname = 'regress_conflict_test1';
 
-- 
2.43.0