From 195f941600ad2cb194d5dd2d4d411b453fd3cfd5 Mon Sep 17 00:00:00 2001
From: shihao zhong <zhong950419@gmail.com>
Date: Mon, 2 Nov 2026 23:23:05 -0500
Subject: [PATCH 2/2] Add test for a failed bulk write inside a savepoint

It uses a new injection point in smgr_bulk_finish().
---
 src/backend/storage/smgr/bulk_write.c         |  2 +
 src/test/modules/test_misc/meson.build        |  1 +
 .../test_misc/t/016_bulk_write_savepoint.pl   | 72 +++++++++++++++++++
 3 files changed, 75 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/016_bulk_write_savepoint.pl

diff --git a/src/backend/storage/smgr/bulk_write.c b/src/backend/storage/smgr/bulk_write.c
index 8a9471d8715..f19ef13066f 100644
--- a/src/backend/storage/smgr/bulk_write.c
+++ b/src/backend/storage/smgr/bulk_write.c
@@ -41,6 +41,7 @@
 #include "storage/bufpage.h"
 #include "storage/bulk_write.h"
 #include "storage/smgr.h"
+#include "utils/injection_point.h"
 #include "utils/rel.h"
 
 #define MAX_PENDING_WRITES XLR_MAX_BLOCK_ID
@@ -203,6 +204,7 @@ smgr_bulk_finish(BulkWriteState *bulkstate)
 		 * DELAY_CHKPT_START, which would stay set if smgrregistersync()
 		 * failed inside a subtransaction.
 		 */
+		INJECTION_POINT("bulk-write-before-registersync", NULL);
 		smgrregistersync(bulkstate->smgr, bulkstate->forknum);
 
 		if (bulkstate->start_RedoRecPtr != GetRedoRecPtr())
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 5d81f5b13be..6ac634ce810 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -24,6 +24,7 @@ tests += {
       't/013_temp_obj_multisession.pl',
       't/014_log_statement_max_length.pl',
       't/015_temp_schema_exit_deferrable.pl',
+      't/016_bulk_write_savepoint.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_bulk_write_savepoint.pl b/src/test/modules/test_misc/t/016_bulk_write_savepoint.pl
new file mode 100644
index 00000000000..de471ae3ee7
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_bulk_write_savepoint.pl
@@ -0,0 +1,72 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Check that a bulk write that fails in smgr_bulk_finish(), with the
+# error caught by a savepoint, does not make checkpoints wait until the
+# top-level transaction ends.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# The injection point is on the path for WAL-logged bulk writes, so
+# wal_level must be higher than minimal.
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(allows_streaming => 1);
+$node->start;
+
+if (!$node->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION injection_points;
+	CREATE TABLE tab (a int);
+	INSERT INTO tab SELECT generate_series(1, 1000);
+));
+
+# Make CREATE INDEX fail in smgr_bulk_finish(), inside a savepoint.
+my $psql = $node->background_psql('postgres', on_error_stop => 0);
+$psql->query_safe(
+	q(SELECT injection_points_set_local();
+	  SELECT injection_points_attach('bulk-write-before-registersync', 'error');
+	  BEGIN;
+	  SAVEPOINT sp;));
+$psql->query('CREATE INDEX tab_a_idx ON tab (a)');
+like(
+	$psql->{stderr},
+	qr/error triggered for injection point bulk-write-before-registersync/,
+	'CREATE INDEX fails in smgr_bulk_finish()');
+$psql->{stderr} = '';
+$psql->query_safe(
+	q(ROLLBACK TO sp;
+	  SELECT injection_points_detach('bulk-write-before-registersync');));
+
+# The transaction is still open.  A checkpoint must not wait for it.
+my $timed_out = 0;
+$node->psql(
+	'postgres', 'CHECKPOINT',
+	timeout => $PostgreSQL::Test::Utils::timeout_default,
+	timed_out => \$timed_out);
+ok(!$timed_out, 'CHECKPOINT completes after ROLLBACK TO SAVEPOINT');
+
+# Another bulk write in the same transaction works.
+$psql->query_safe(
+	q(CREATE INDEX tab_a_idx ON tab (a);
+	  COMMIT;));
+is( $node->safe_psql(
+		'postgres', "SELECT count(*) FROM pg_index WHERE indexrelid = 'tab_a_idx'::regclass"),
+	'1',
+	'second CREATE INDEX in the same transaction succeeds');
+
+$psql->quit;
+done_testing();
-- 
2.37.1 (Apple Git-137.1)

