From ce459ae4d8a1b579850d26755164742fc10242de Mon Sep 17 00:00:00 2001 From: Zhong ShiHao Date: Fri, 4 Sep 2026 22:11:27 -0400 Subject: [PATCH 2/2] Add a recovery test for GiST page deletion and reuse No test replayed a GiST page deletion or reuse through recovery. --- src/test/recovery/meson.build | 1 + .../recovery/t/057_gist_crash_recovery.pl | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/test/recovery/t/057_gist_crash_recovery.pl diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index 72113c5ac6e..4df357313f5 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -65,6 +65,7 @@ tests += { 't/054_unlogged_sequence_promotion.pl', 't/055_cascade_reconnect.pl', 't/056_standby_snapshot_export.pl', + 't/057_gist_crash_recovery.pl', ], }, } diff --git a/src/test/recovery/t/057_gist_crash_recovery.pl b/src/test/recovery/t/057_gist_crash_recovery.pl new file mode 100644 index 00000000000..774626bf670 --- /dev/null +++ b/src/test/recovery/t/057_gist_crash_recovery.pl @@ -0,0 +1,99 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Crash recovery must replay GiST page splits, page deletion, page recycling +# and page reuse correctly, leaving the index consistent with the heap. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init; + +# Keep every GiST record after our checkpoint inside the recovery range, and +# check each replayed page against its full-page image. +$node->append_conf( + 'postgresql.conf', q{ +autovacuum = off +checkpoint_timeout = 1h +max_wal_size = 4GB +wal_consistency_checking = gist +}); +$node->start; + +$node->safe_psql( + 'postgres', q{ +CREATE TABLE gist_crash (id int4, p point); +CREATE INDEX gist_crash_idx ON gist_crash USING gist (p); +}); + +# Everything from here on is replayed by crash recovery. +$node->safe_psql('postgres', 'CHECKPOINT'); + +my $where = 'p <@ box(point(0,0), point(1000000,1000000))'; + +# Return the same query's result through an index scan and a sequential scan. +sub checksums +{ + my ($node) = @_; + my $agg = "count(*) || ' ' || coalesce(md5(string_agg(id::text, ',' ORDER BY id)), 'empty')"; + my $idx = $node->safe_psql('postgres', + "SET enable_seqscan = off; SET enable_bitmapscan = off; + SELECT $agg FROM gist_crash WHERE $where"); + my $seq = $node->safe_psql('postgres', + "SET enable_indexscan = off; SET enable_bitmapscan = off; + SET enable_indexonlyscan = off; + SELECT $agg FROM gist_crash WHERE $where"); + return ($idx, $seq); +} + +# Page splits while growing the index. +$node->safe_psql('postgres', + 'INSERT INTO gist_crash SELECT g, point(g*10, g*10) FROM generate_series(1, 20000) g' +); + +# Remove tuples from leaf pages that stay in use, then empty a range of leaf +# pages so that they get deleted. +$node->safe_psql('postgres', 'DELETE FROM gist_crash WHERE id % 2 = 1'); +$node->safe_psql('postgres', 'DELETE FROM gist_crash WHERE id > 5000'); +$node->safe_psql('postgres', 'VACUUM gist_crash'); + +# Move the deleted pages behind the xid horizon, recycle them, then reuse them. +$node->safe_psql('postgres', 'SELECT pg_current_xact_id()'); +$node->safe_psql('postgres', 'VACUUM gist_crash'); +$node->safe_psql('postgres', + 'INSERT INTO gist_crash SELECT g, point(g*10, g*10) FROM generate_series(5001, 8000) g' +); + +my ($idx_before, $seq_before) = checksums($node); +is($idx_before, $seq_before, + 'index scan matches sequential scan before crash'); + +$node->stop('immediate'); +$node->start; + +my $log = slurp_file($node->logfile); +like($log, qr/redo starts at/, 'crash recovery replayed WAL'); +unlike($log, qr/inconsistent page found/, + 'no WAL consistency check failure during replay'); + +my ($idx_after, $seq_after) = checksums($node); +is($idx_after, $idx_before, 'index scan unchanged by crash recovery'); +is($idx_after, $seq_after, + 'index scan matches sequential scan after crash recovery'); + +# The recovered index must still be usable through another round of page +# deletion and reuse. +$node->safe_psql('postgres', 'DELETE FROM gist_crash'); +$node->safe_psql('postgres', 'VACUUM gist_crash'); +$node->safe_psql('postgres', + 'INSERT INTO gist_crash SELECT g, point(g*10, g*10) FROM generate_series(1, 3000) g' +); +my ($idx_final, $seq_final) = checksums($node); +is($idx_final, $seq_final, + 'index scan matches sequential scan after reuse following recovery'); + +$node->stop; +done_testing(); -- 2.37.1 (Apple Git-137.1)