From 743a6d8e625d48f06fa8d990da789f1b04a4be92 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Tue, 28 Apr 2026 16:37:15 +0530
Subject: [PATCH v1] Fix stale relation close in sequence synchronization

copy_sequences() passes a Relation pointer to get_and_validate_seq_info()
for each publisher result row and closes the relation after processing the
row. If pg_get_sequence_data() returned NULL sequence data for a later row,
get_and_validate_seq_info() could return COPYSEQ_SKIPPED before assigning a
new relation to that output argument. The caller would then close the stale
Relation pointer left from the previous row, tripping the relcache refcount
assertion in assert-enabled builds.

This is not limited to concurrent drops. pg_get_sequence_data() also returns
NULL data when the publisher connection user cannot read the sequence.

Clear the output Relation before validation and clear the local pointer after
closing it. Add a subscription test that syncs one readable sequence followed
by a published sequence for which the replication user lacks SELECT.
---
 .../replication/logical/sequencesync.c        |  9 +++--
 src/test/subscription/t/036_sequences.pl      | 36 +++++++++++++++++++
 2 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c
index ec7e76abf93..29264f8d01c 100644
--- a/src/backend/replication/logical/sequencesync.c
+++ b/src/backend/replication/logical/sequencesync.c
@@ -246,6 +246,8 @@ get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel,
 	Form_pg_sequence local_seq;
 	LogicalRepSequenceInfo *seqinfo_local;
 
+	*sequence_rel = NULL;
+
 	*seqidx = DatumGetInt32(slot_getattr(slot, ++col, &isnull));
 	Assert(!isnull);
 
@@ -254,8 +256,8 @@ get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel,
 		(LogicalRepSequenceInfo *) list_nth(seqinfos, *seqidx);
 
 	/*
-	 * last_value can be NULL if the sequence was dropped concurrently (see
-	 * pg_get_sequence_data()).
+	 * The sequence data can be NULL if it is not accessible on the publisher
+	 * (see pg_get_sequence_data()).
 	 */
 	datum = slot_getattr(slot, ++col, &isnull);
 	if (isnull)
@@ -565,7 +567,10 @@ copy_sequences(WalReceiverConn *conn)
 			}
 
 			if (sequence_rel)
+			{
 				table_close(sequence_rel, NoLock);
+				sequence_rel = NULL;
+			}
 		}
 
 		ExecDropSingleTupleTableSlot(slot);
diff --git a/src/test/subscription/t/036_sequences.pl b/src/test/subscription/t/036_sequences.pl
index 471780a3585..53233920edf 100644
--- a/src/test/subscription/t/036_sequences.pl
+++ b/src/test/subscription/t/036_sequences.pl
@@ -221,4 +221,40 @@ $node_subscriber->wait_for_log(
 	qr/WARNING: ( [A-Z0-9]+:)? missing sequence on publisher \("public.regress_s4"\)/,
 	$log_offset);
 
+##########
+# A NULL sequence data row from the publisher must not make the subscriber
+# close the previously synchronized sequence relation again.
+##########
+
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE ROLE regress_seq_repl LOGIN REPLICATION;
+	CREATE SEQUENCE regress_no_select;
+	SELECT nextval('regress_no_select');
+	GRANT USAGE ON SCHEMA public TO regress_seq_repl;
+	GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO regress_seq_repl;
+	REVOKE ALL ON SEQUENCE regress_no_select FROM PUBLIC;
+	REVOKE ALL ON SEQUENCE regress_no_select FROM regress_seq_repl;
+));
+
+$node_subscriber->safe_psql('postgres',
+	qq(CREATE SEQUENCE regress_no_select;));
+
+my $publisher_limited_connstr =
+  $node_publisher->connstr . ' dbname=postgres user=regress_seq_repl';
+$log_offset = -s $node_subscriber->logfile;
+
+$node_subscriber->safe_psql(
+	'postgres',
+	"CREATE SUBSCRIPTION regress_seq_sub_no_select CONNECTION '$publisher_limited_connstr' PUBLICATION regress_seq_pub WITH (disable_on_error = true)"
+);
+
+$node_subscriber->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? missing sequence on publisher \("public.regress_no_select"\)/,
+	$log_offset);
+
+$result = $node_subscriber->safe_psql('postgres', 'SELECT 1');
+is($result, '1',
+	'subscriber remains running after publisher returns NULL sequence data');
+
 done_testing();
-- 
2.34.1

