1
2# Copyright (c) 2021, PostgreSQL Global Development Group
3
4use strict;
5use warnings;
6
7use PostgresNode;
8use TestLib;
9
10use Fcntl qw(:seek);
11use Test::More tests => 80;
12
13my ($node, $result);
14
15#
16# Test set-up
17#
18$node = get_new_node('test');
19$node->init;
20$node->append_conf('postgresql.conf', 'autovacuum=off');
21$node->start;
22$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
23
24#
25# Check a table with data loaded but no corruption, freezing, etc.
26#
27fresh_test_table('test');
28check_all_options_uncorrupted('test', 'plain');
29
30#
31# Check a corrupt table
32#
33fresh_test_table('test');
34corrupt_first_page('test');
35detects_heap_corruption("verify_heapam('test')", "plain corrupted table");
36detects_heap_corruption(
37	"verify_heapam('test', skip := 'all-visible')",
38	"plain corrupted table skipping all-visible");
39detects_heap_corruption(
40	"verify_heapam('test', skip := 'all-frozen')",
41	"plain corrupted table skipping all-frozen");
42detects_heap_corruption(
43	"verify_heapam('test', check_toast := false)",
44	"plain corrupted table skipping toast");
45detects_heap_corruption(
46	"verify_heapam('test', startblock := 0, endblock := 0)",
47	"plain corrupted table checking only block zero");
48
49#
50# Check a corrupt table with all-frozen data
51#
52fresh_test_table('test');
53$node->safe_psql('postgres', q(VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test));
54detects_no_corruption("verify_heapam('test')",
55	"all-frozen not corrupted table");
56corrupt_first_page('test');
57detects_heap_corruption("verify_heapam('test')",
58	"all-frozen corrupted table");
59detects_no_corruption(
60	"verify_heapam('test', skip := 'all-frozen')",
61	"all-frozen corrupted table skipping all-frozen");
62
63# Returns the filesystem path for the named relation.
64sub relation_filepath
65{
66	my ($relname) = @_;
67
68	my $pgdata = $node->data_dir;
69	my $rel    = $node->safe_psql('postgres',
70		qq(SELECT pg_relation_filepath('$relname')));
71	die "path not found for relation $relname" unless defined $rel;
72	return "$pgdata/$rel";
73}
74
75# Returns the fully qualified name of the toast table for the named relation
76sub get_toast_for
77{
78	my ($relname) = @_;
79
80	return $node->safe_psql(
81		'postgres', qq(
82		SELECT 'pg_toast.' || t.relname
83			FROM pg_catalog.pg_class c, pg_catalog.pg_class t
84			WHERE c.relname = '$relname'
85			  AND c.reltoastrelid = t.oid));
86}
87
88# (Re)create and populate a test table of the given name.
89sub fresh_test_table
90{
91	my ($relname) = @_;
92
93	return $node->safe_psql(
94		'postgres', qq(
95		DROP TABLE IF EXISTS $relname CASCADE;
96		CREATE TABLE $relname (a integer, b text);
97		ALTER TABLE $relname SET (autovacuum_enabled=false);
98		ALTER TABLE $relname ALTER b SET STORAGE external;
99		INSERT INTO $relname (a, b)
100			(SELECT gs, repeat('b',gs*10) FROM generate_series(1,1000) gs);
101		BEGIN;
102		SAVEPOINT s1;
103		SELECT 1 FROM $relname WHERE a = 42 FOR UPDATE;
104		UPDATE $relname SET b = b WHERE a = 42;
105		RELEASE s1;
106		SAVEPOINT s1;
107		SELECT 1 FROM $relname WHERE a = 42 FOR UPDATE;
108		UPDATE $relname SET b = b WHERE a = 42;
109		COMMIT;
110	));
111}
112
113# Stops the test node, corrupts the first page of the named relation, and
114# restarts the node.
115sub corrupt_first_page
116{
117	my ($relname) = @_;
118	my $relpath = relation_filepath($relname);
119
120	$node->stop;
121
122	my $fh;
123	open($fh, '+<', $relpath)
124	  or BAIL_OUT("open failed: $!");
125	binmode $fh;
126
127	# Corrupt some line pointers.  The values are chosen to hit the
128	# various line-pointer-corruption checks in verify_heapam.c
129	# on both little-endian and big-endian architectures.
130	seek($fh, 32, SEEK_SET)
131	  or BAIL_OUT("seek failed: $!");
132	syswrite(
133		$fh,
134		pack("L*",
135			0xAAA15550, 0xAAA0D550, 0x00010000,
136			0x00008000, 0x0000800F, 0x001e8000)
137	) or BAIL_OUT("syswrite failed: $!");
138	close($fh)
139	  or BAIL_OUT("close failed: $!");
140
141	$node->start;
142}
143
144sub detects_heap_corruption
145{
146	local $Test::Builder::Level = $Test::Builder::Level + 1;
147
148	my ($function, $testname) = @_;
149
150	detects_corruption(
151		$function,
152		$testname,
153		qr/line pointer redirection to item at offset \d+ precedes minimum offset \d+/,
154		qr/line pointer redirection to item at offset \d+ exceeds maximum offset \d+/,
155		qr/line pointer to page offset \d+ is not maximally aligned/,
156		qr/line pointer length \d+ is less than the minimum tuple header size \d+/,
157		qr/line pointer to page offset \d+ with length \d+ ends beyond maximum page offset \d+/,
158	);
159}
160
161sub detects_corruption
162{
163	local $Test::Builder::Level = $Test::Builder::Level + 1;
164
165	my ($function, $testname, @re) = @_;
166
167	my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
168	like($result, $_, $testname) for (@re);
169}
170
171sub detects_no_corruption
172{
173	local $Test::Builder::Level = $Test::Builder::Level + 1;
174
175	my ($function, $testname) = @_;
176
177	my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
178	is($result, '', $testname);
179}
180
181# Check various options are stable (don't abort) and do not report corruption
182# when running verify_heapam on an uncorrupted test table.
183#
184# The relname *must* be an uncorrupted table, or this will fail.
185#
186# The prefix is used to identify the test, along with the options,
187# and should be unique.
188sub check_all_options_uncorrupted
189{
190	local $Test::Builder::Level = $Test::Builder::Level + 1;
191
192	my ($relname, $prefix) = @_;
193
194	for my $stop (qw(true false))
195	{
196		for my $check_toast (qw(true false))
197		{
198			for my $skip ("'none'", "'all-frozen'", "'all-visible'")
199			{
200				for my $startblock (qw(NULL 0))
201				{
202					for my $endblock (qw(NULL 0))
203					{
204						my $opts =
205						    "on_error_stop := $stop, "
206						  . "check_toast := $check_toast, "
207						  . "skip := $skip, "
208						  . "startblock := $startblock, "
209						  . "endblock := $endblock";
210
211						detects_no_corruption(
212							"verify_heapam('$relname', $opts)",
213							"$prefix: $opts");
214					}
215				}
216			}
217		}
218	}
219}
220