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