From 849222d30ec9cce21ca16ed42a3944074d234653 Mon Sep 17 00:00:00 2001
From: debebantur <stepan.filippov@yandex.ru>
Date: Fri, 31 Jul 2026 08:11:23 +0000
Subject: [[PATCH] Fix timeline history after recovery stops on an ancestor] A
 recovery target can be reached before recovery switches to the requested
 timeline.  Using recoveryTargetTLI as the parent in that case creates a
 history file that claims the new timeline forked from a timeline that
 recovery never reached.Use the timeline of the last replayed WAL record as
 the parent instead.

Extend the existing PITR timeline test to verify the resulting history.
---
 src/backend/access/transam/xlog.c         |  2 +-
 src/test/recovery/t/002_archiving.pl      | 14 ++++++++++++++
 src/test/recovery/t/028_pitr_timelines.pl |  9 +++++++++
 3 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f85b528608..6b0b27d853 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -6408,7 +6408,7 @@ StartupXLOG(void)
 		 * To minimize the window for that, try to do as little as possible
 		 * between here and writing the end-of-recovery record.
 		 */
-		writeTimeLineHistory(newTLI, recoveryTargetTLI,
+		writeTimeLineHistory(newTLI, endOfRecoveryInfo->lastRecTLI,
 							 EndOfLog, endOfRecoveryInfo->recoveryStopReason);
 
 		ereport(LOG,
diff --git a/src/test/recovery/t/002_archiving.pl b/src/test/recovery/t/002_archiving.pl
index aa40f58e6d..a7ff14b3a0 100644
--- a/src/test/recovery/t/002_archiving.pl
+++ b/src/test/recovery/t/002_archiving.pl
@@ -100,6 +100,20 @@ $caughtup_query =
 $node_primary->poll_query_until('postgres', $caughtup_query)
   or die "Timed out while waiting for archiving of 00000002.history";
 
+# Generate and archive some WAL on timeline 2.  This ensures that standby2
+# actually reaches timeline 2, so that its history file is needed again when
+# standby2 is promoted.
+$node_standby->safe_psql('postgres', "CREATE TABLE timeline_2_marker (a int)");
+my $timeline_2_lsn =
+  $node_standby->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+my $timeline_2_walfile = $node_standby->safe_psql(
+	'postgres', "SELECT pg_walfile_name('$timeline_2_lsn')");
+$node_standby->safe_psql('postgres', "SELECT pg_switch_wal()");
+$caughtup_query =
+  "SELECT size IS NOT NULL FROM pg_stat_file('$primary_archive/$timeline_2_walfile', true)";
+$node_primary->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for archiving of $timeline_2_walfile";
+
 # recovery_end_command should have been triggered on promotion.
 ok( -f "$data_dir/$recovery_end_command_file",
 	'recovery_end_command executed after promotion');
diff --git a/src/test/recovery/t/028_pitr_timelines.pl b/src/test/recovery/t/028_pitr_timelines.pl
index ae617f6195..429b6e8220 100644
--- a/src/test/recovery/t/028_pitr_timelines.pl
+++ b/src/test/recovery/t/028_pitr_timelines.pl
@@ -150,6 +150,15 @@ $node_pitr->poll_query_until('postgres',
 # Stop the node.  This archives the last segment.
 $node_pitr->stop();
 
+# The new timeline branched from timeline 1 at the recovery target, before
+# timeline 2 branched from timeline 1.  Its history must therefore name
+# timeline 1 as its direct parent, rather than copying timeline 2's history.
+my $history = slurp_file($node_primary->archive_dir . '/00000003.history');
+like(
+	$history,
+	qr/^1\t[0-9A-F]+\/[0-9A-F]+\t[^\r\n]*(?:\r?\n)?\z/,
+	"new timeline history has the timeline containing the target as parent");
+
 # Test archive recovery on the timeline created by the PITR.  This
 # replays the end-of-recovery record that switches from timeline 1 to
 # 3.
-- 
2.34.1

