From 3e734ca8eab6083f6a27ea560e2fefd834f8ee73 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 16 Dec 2025 09:16:26 +0530
Subject: [PATCH] pg_dump: dump conflict log table configuration for
 subscriptions

Allow pg_dump to preserve the conflict_log_table setting of logical
replication subscriptions.
---
 src/backend/commands/subscriptioncmds.c    | 33 +++++++++++++-
 src/bin/pg_dump/pg_dump.c                  | 52 +++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h                  |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl           |  5 ++-
 src/test/regress/expected/subscription.out |  5 ++-
 5 files changed, 89 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eb3fe068ddb..e30078c351c 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -1765,7 +1765,38 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 						 * provided.
 						 */
 						if (relname != NULL)
-							create_conflict_log_table(nspid, relname, subid);
+						{
+							Oid conflictlogrelid = get_relname_relid(relname, nspid);
+							if (OidIsValid(conflictlogrelid))
+							{
+								Relation conflictlogrel;
+
+								conflictlogrel = table_open(conflictlogrelid,
+															RowExclusiveLock);
+								if (IsConflictLogTable(conflictlogrelid))
+									ereport(ERROR,
+											errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+											errmsg("conflict log table \"%s.%s\" cannot be used",
+												get_namespace_name(RelationGetNamespace(conflictlogrel)),
+												RelationGetRelationName(conflictlogrel)),
+											errdetail("The specified table is already registered for a different subscription."),
+											errhint("Specify a different conflict log table."));
+
+								if (!ValidateConflictLogTable(conflictlogrel))
+									ereport(ERROR,
+											errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+											errmsg("conflict log table \"%s.%s\" has an incompatible definition",
+													get_namespace_name(RelationGetNamespace(conflictlogrel)),
+													RelationGetRelationName(conflictlogrel)),
+											errdetail("The table does not match the required conflict log table structure."),
+											errhint("Create the conflict log table with the expected definition or specify a different table."));
+
+								table_close(conflictlogrel, NoLock);
+
+							}
+							else
+								create_conflict_log_table(nspid, relname, subid);
+						}
 
 						values[Anum_pg_subscription_subconflictlognspid - 1] =
 									ObjectIdGetDatum(nspid);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ad201af2f..1e286e531b2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5130,6 +5130,7 @@ getSubscriptions(Archive *fout)
 	int			i_subfailover;
 	int			i_subretaindeadtuples;
 	int			i_submaxretention;
+	int			i_subconflictlogrelid;
 	int			i,
 				ntups;
 
@@ -5216,10 +5217,17 @@ getSubscriptions(Archive *fout)
 
 	if (fout->remoteVersion >= 190000)
 		appendPQExpBufferStr(query,
-							 " s.submaxretention\n");
+							 " s.submaxretention,\n");
 	else
 		appendPQExpBuffer(query,
-						  " 0 AS submaxretention\n");
+						  " 0 AS submaxretention,\n");
+
+	if (fout->remoteVersion >= 190000)
+		appendPQExpBufferStr(query,
+							 " c.oid AS subconflictlogrelid\n");
+	else
+		appendPQExpBufferStr(query,
+							 " 0::oid AS subconflictlogrelid\n");
 
 	appendPQExpBufferStr(query,
 						 "FROM pg_subscription s\n");
@@ -5229,6 +5237,12 @@ getSubscriptions(Archive *fout)
 							 "LEFT JOIN pg_catalog.pg_replication_origin_status o \n"
 							 "    ON o.external_id = 'pg_' || s.oid::text \n");
 
+	if (fout->remoteVersion >= 190000)
+		appendPQExpBufferStr(query,
+							 "LEFT JOIN pg_class c ON c.relname = s.subconflictlogtable\n"
+							 "LEFT JOIN pg_namespace n \n"
+							 "    ON n.oid = c.relnamespace AND n.oid = s.subconflictlognspid\n");
+
 	appendPQExpBufferStr(query,
 						 "WHERE s.subdbid = (SELECT oid FROM pg_database\n"
 						 "                   WHERE datname = current_database())");
@@ -5255,6 +5269,7 @@ getSubscriptions(Archive *fout)
 	i_subfailover = PQfnumber(res, "subfailover");
 	i_subretaindeadtuples = PQfnumber(res, "subretaindeadtuples");
 	i_submaxretention = PQfnumber(res, "submaxretention");
+	i_subconflictlogrelid = PQfnumber(res, "subconflictlogrelid");
 	i_subconninfo = PQfnumber(res, "subconninfo");
 	i_subslotname = PQfnumber(res, "subslotname");
 	i_subsynccommit = PQfnumber(res, "subsynccommit");
@@ -5292,6 +5307,22 @@ getSubscriptions(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_subretaindeadtuples), "t") == 0);
 		subinfo[i].submaxretention =
 			atoi(PQgetvalue(res, i, i_submaxretention));
+		subinfo[i].subconflictlogrelid =
+			atooid(PQgetvalue(res, i, i_subconflictlogrelid));
+
+		if (subinfo[i].subconflictlogrelid != InvalidOid)
+		{
+			TableInfo  *tableInfo = findTableByOid(subinfo[i].subconflictlogrelid);
+
+			if (!tableInfo)
+				pg_fatal("could not find conflict log table with OID %u",
+						 subinfo[i].subconflictlogrelid);
+
+			/* Ensure the table is marked to be dumped */
+			tableInfo->dobj.dump |= DUMP_COMPONENT_DEFINITION;
+
+			addObjectDependency(&subinfo[i].dobj, tableInfo->dobj.dumpId);
+		}
 		subinfo[i].subconninfo =
 			pg_strdup(PQgetvalue(res, i, i_subconninfo));
 		if (PQgetisnull(res, i, i_subslotname))
@@ -5564,6 +5595,23 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 
 	appendPQExpBufferStr(query, ");\n");
 
+	if (subinfo->subconflictlogrelid != InvalidOid)
+	{
+		PQExpBuffer conflictlogbuf = createPQExpBuffer();
+		TableInfo  *tbinfo = findTableByOid(subinfo->subconflictlogrelid);
+
+		appendStringLiteralAH(conflictlogbuf,
+							  fmtQualifiedDumpable(tbinfo),
+							  fout);
+
+		appendPQExpBuffer(query,
+						  "\n\nALTER SUBSCRIPTION %s SET (conflict_log_table = %s);\n",
+						  qsubname,
+						  conflictlogbuf->data);
+
+		destroyPQExpBuffer(conflictlogbuf);
+	}
+
 	/*
 	 * In binary-upgrade mode, we allow the replication to continue after the
 	 * upgrade.
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 72a00e1bc20..20ffae491eb 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -719,6 +719,7 @@ typedef struct _SubscriptionInfo
 	bool		subfailover;
 	bool		subretaindeadtuples;
 	int			submaxretention;
+	Oid			subconflictlogrelid;
 	char	   *subconninfo;
 	char	   *subslotname;
 	char	   *subsynccommit;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e33aa95f6ff..ef11db6b8ee 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3204,9 +3204,10 @@ my %tests = (
 		create_order => 50,
 		create_sql => 'CREATE SUBSCRIPTION sub3
 						 CONNECTION \'dbname=doesnotexist\' PUBLICATION pub1
-						 WITH (connect = false, origin = any, streaming = on);',
+						 WITH (connect = false, origin = any, streaming = on, conflict_log_table = \'conflict\');',
 		regexp => qr/^
-			\QCREATE SUBSCRIPTION sub3 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub3', streaming = on);\E
+			\QCREATE SUBSCRIPTION sub3 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub3', streaming = on);\E\n\n\n
+			\QALTER SUBSCRIPTION sub3 SET (conflict_log_table = 'public.conflict');\E
 			/xm,
 		like => { %full_runs, section_post_data => 1, },
 		unlike => {
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index bab2d0ea954..64ee5b9d43e 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -630,8 +630,9 @@ SELECT * FROM pg_publication_tables WHERE pubname = 'pub';
 DROP PUBLICATION pub;
 -- fail - set conflict_log_table to one already used by a different subscription
 ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_table = 'public.regress_conflict_log1');
-ERROR:  cannot create conflict log table "public.regress_conflict_log1" because a table with that name already exists
-HINT:  Use a different name for the conflict log table or drop the existing table.
+ERROR:  conflict log table "public.regress_conflict_log1" cannot be used
+DETAIL:  The specified table is already registered for a different subscription.
+HINT:  Specify a different conflict log table.
 -- ok - dropping subscription also drops the log table
 ALTER SUBSCRIPTION regress_conflict_test1 DISABLE;
 ALTER SUBSCRIPTION regress_conflict_test1 SET (slot_name = NONE);
-- 
2.43.0

