# Copyright (c) 2026, PostgreSQL Global Development Group

# An error raised between the WAL record and the file extension in
# _hash_alloc_buckets() must not leave a checkpoint waiting.
#
# Expected: the checkpoint completes.  This FAILS while the flag stays set.
#
# Not run.

use strict;
use warnings FATAL => 'all';

use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
use Time::HiRes qw(usleep);

if ($ENV{enable_injection_points} ne 'yes')
{
	plan skip_all => 'Injection points not supported by this build';
}

my $node = PostgreSQL::Test::Cluster->new('main');
$node->init;
$node->append_conf('postgresql.conf', 'autovacuum = off');
$node->start;

if (!$node->check_extension('injection_points'))
{
	plan skip_all => 'Extension injection_points not installed';
}

$node->safe_psql('postgres', 'CREATE EXTENSION injection_points');
$node->safe_psql(
	'postgres', q(
	CREATE TABLE hash_tab (k int);
	CREATE INDEX hash_idx ON hash_tab USING hash (k);
	INSERT INTO hash_tab SELECT (random() * 1e9)::int
	  FROM generate_series(1, 20000);
));

# The session that hits the error.  The injection point is attached for this
# backend only.
my $s = $node->background_psql('postgres', on_error_stop => 0);
my $s_pid = $s->query_safe('SELECT pg_backend_pid()');
$s->query_safe(
	q(SELECT injection_points_set_local();
	  SELECT injection_points_attach('hash-alloc-buckets-after-wal', 'error');));

# Insert until the hash index needs a new splitpoint, where _hash_alloc_buckets()
# raises the error.  The loop is bounded, so that a failure to reach the point
# shows up as a failed premise and not as a hang.
$s->query_safe('BEGIN; SAVEPOINT s;');
$s->query(q{
DO $$
BEGIN
  FOR i IN 1..3000 LOOP
    INSERT INTO hash_tab SELECT (random() * 1e9)::int FROM generate_series(1, 1000);
  END LOOP;
END $$;
});
like($s->{stderr},
	qr/error triggered for injection point hash-alloc-buckets-after-wal/,
	'premise: _hash_alloc_buckets() failed between the WAL record and the extension');

# The error is caught by the savepoint, and the transaction goes on.
$s->{stderr} = '';
$s->query_safe('ROLLBACK TO s;');
is( $node->safe_psql(
		'postgres', "SELECT state FROM pg_stat_activity WHERE pid = $s_pid"),
	'idle in transaction',
	'premise: the transaction is still open');

# A checkpoint now.  It has no reason to wait: no checkpoint-delaying section is
# in progress.
my $ckpt = $node->background_psql('postgres', on_error_stop => 0);
my $ckpt_pid = $ckpt->query_safe('SELECT pg_backend_pid()');
$ckpt->query_until(
	qr/starting_checkpoint/, q(
	\echo starting_checkpoint
	CHECKPOINT;
));

my ($stuck, $done) = (0, 0);
for (1 .. 150)
{
	if ($node->safe_psql(
			'postgres',
			"SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'checkpointer' AND wait_event = 'CheckpointDelayStart'"
		) eq '1')
	{
		$stuck = 1;
		last;
	}
	if ($node->safe_psql('postgres',
			"SELECT state FROM pg_stat_activity WHERE pid = $ckpt_pid") eq 'idle')
	{
		$done = 1;
		last;
	}
	usleep(100_000);
}

ok(!$stuck,
	'the checkpoint does not wait for a transaction that rolled back to a savepoint');
ok($done, 'the checkpoint completed');

# Clean up: the abort of the transaction clears the flag, if it was left set.
$s->query_safe('ROLLBACK;');
$ckpt->query_safe('SELECT 1');
$ckpt->quit;
$s->quit;

$node->stop;
done_testing();
