mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-29 22:49:41 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			287 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| 
 | |
| # Copyright (c) 2021-2023, PostgreSQL Global Development Group
 | |
| 
 | |
| use strict;
 | |
| use warnings;
 | |
| 
 | |
| use PostgreSQL::Test::Cluster;
 | |
| use PostgreSQL::Test::Utils;
 | |
| 
 | |
| use Test::More;
 | |
| 
 | |
| my ($node, $result);
 | |
| 
 | |
| #
 | |
| # Test set-up
 | |
| #
 | |
| $node = PostgreSQL::Test::Cluster->new('test');
 | |
| $node->init;
 | |
| $node->append_conf('postgresql.conf', 'autovacuum=off');
 | |
| $node->start;
 | |
| $node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
 | |
| 
 | |
| #
 | |
| # Check a table with data loaded but no corruption, freezing, etc.
 | |
| #
 | |
| fresh_test_table('test');
 | |
| check_all_options_uncorrupted('test', 'plain');
 | |
| 
 | |
| #
 | |
| # Check a corrupt table
 | |
| #
 | |
| fresh_test_table('test');
 | |
| corrupt_first_page('test');
 | |
| detects_heap_corruption("verify_heapam('test')", "plain corrupted table");
 | |
| detects_heap_corruption(
 | |
| 	"verify_heapam('test', skip := 'all-visible')",
 | |
| 	"plain corrupted table skipping all-visible");
 | |
| detects_heap_corruption(
 | |
| 	"verify_heapam('test', skip := 'all-frozen')",
 | |
| 	"plain corrupted table skipping all-frozen");
 | |
| detects_heap_corruption(
 | |
| 	"verify_heapam('test', check_toast := false)",
 | |
| 	"plain corrupted table skipping toast");
 | |
| detects_heap_corruption(
 | |
| 	"verify_heapam('test', startblock := 0, endblock := 0)",
 | |
| 	"plain corrupted table checking only block zero");
 | |
| 
 | |
| #
 | |
| # Check a corrupt table with all-frozen data
 | |
| #
 | |
| fresh_test_table('test');
 | |
| $node->safe_psql('postgres', q(VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test));
 | |
| detects_no_corruption("verify_heapam('test')",
 | |
| 	"all-frozen not corrupted table");
 | |
| corrupt_first_page('test');
 | |
| detects_heap_corruption("verify_heapam('test')",
 | |
| 	"all-frozen corrupted table");
 | |
| detects_no_corruption(
 | |
| 	"verify_heapam('test', skip := 'all-frozen')",
 | |
| 	"all-frozen corrupted table skipping all-frozen");
 | |
| 
 | |
| #
 | |
| # Check a sequence with no corruption.  The current implementation of sequences
 | |
| # doesn't require its own test setup, since sequences are really just heap
 | |
| # tables under-the-hood.  To guard against future implementation changes made
 | |
| # without remembering to update verify_heapam, we create and exercise a
 | |
| # sequence, checking along the way that it passes corruption checks.
 | |
| #
 | |
| fresh_test_sequence('test_seq');
 | |
| check_all_options_uncorrupted('test_seq', 'plain');
 | |
| advance_test_sequence('test_seq');
 | |
| check_all_options_uncorrupted('test_seq', 'plain');
 | |
| set_test_sequence('test_seq');
 | |
| check_all_options_uncorrupted('test_seq', 'plain');
 | |
| reset_test_sequence('test_seq');
 | |
| check_all_options_uncorrupted('test_seq', 'plain');
 | |
| 
 | |
| # Returns the filesystem path for the named relation.
 | |
| sub relation_filepath
 | |
| {
 | |
| 	my ($relname) = @_;
 | |
| 
 | |
| 	my $pgdata = $node->data_dir;
 | |
| 	my $rel    = $node->safe_psql('postgres',
 | |
| 		qq(SELECT pg_relation_filepath('$relname')));
 | |
| 	die "path not found for relation $relname" unless defined $rel;
 | |
| 	return "$pgdata/$rel";
 | |
| }
 | |
| 
 | |
| # Returns the fully qualified name of the toast table for the named relation
 | |
| sub get_toast_for
 | |
| {
 | |
| 	my ($relname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		SELECT 'pg_toast.' || t.relname
 | |
| 			FROM pg_catalog.pg_class c, pg_catalog.pg_class t
 | |
| 			WHERE c.relname = '$relname'
 | |
| 			  AND c.reltoastrelid = t.oid));
 | |
| }
 | |
| 
 | |
| # (Re)create and populate a test table of the given name.
 | |
| sub fresh_test_table
 | |
| {
 | |
| 	my ($relname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		DROP TABLE IF EXISTS $relname CASCADE;
 | |
| 		CREATE TABLE $relname (a integer, b text);
 | |
| 		ALTER TABLE $relname SET (autovacuum_enabled=false);
 | |
| 		ALTER TABLE $relname ALTER b SET STORAGE external;
 | |
| 		INSERT INTO $relname (a, b)
 | |
| 			(SELECT gs, repeat('b',gs*10) FROM generate_series(1,1000) gs);
 | |
| 		BEGIN;
 | |
| 		SAVEPOINT s1;
 | |
| 		SELECT 1 FROM $relname WHERE a = 42 FOR UPDATE;
 | |
| 		UPDATE $relname SET b = b WHERE a = 42;
 | |
| 		RELEASE s1;
 | |
| 		SAVEPOINT s1;
 | |
| 		SELECT 1 FROM $relname WHERE a = 42 FOR UPDATE;
 | |
| 		UPDATE $relname SET b = b WHERE a = 42;
 | |
| 		COMMIT;
 | |
| 	));
 | |
| }
 | |
| 
 | |
| # Create a test sequence of the given name.
 | |
| sub fresh_test_sequence
 | |
| {
 | |
| 	my ($seqname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		DROP SEQUENCE IF EXISTS $seqname CASCADE;
 | |
| 		CREATE SEQUENCE $seqname
 | |
| 			INCREMENT BY 13
 | |
| 			MINVALUE 17
 | |
| 			START WITH 23;
 | |
| 		SELECT nextval('$seqname');
 | |
| 		SELECT setval('$seqname', currval('$seqname') + nextval('$seqname'));
 | |
| 	));
 | |
| }
 | |
| 
 | |
| # Call SQL functions to increment the sequence
 | |
| sub advance_test_sequence
 | |
| {
 | |
| 	my ($seqname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		SELECT nextval('$seqname');
 | |
| 	));
 | |
| }
 | |
| 
 | |
| # Call SQL functions to set the sequence
 | |
| sub set_test_sequence
 | |
| {
 | |
| 	my ($seqname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		SELECT setval('$seqname', 102);
 | |
| 	));
 | |
| }
 | |
| 
 | |
| # Call SQL functions to reset the sequence
 | |
| sub reset_test_sequence
 | |
| {
 | |
| 	my ($seqname) = @_;
 | |
| 
 | |
| 	return $node->safe_psql(
 | |
| 		'postgres', qq(
 | |
| 		ALTER SEQUENCE $seqname RESTART WITH 51
 | |
| 	));
 | |
| }
 | |
| 
 | |
| # Stops the test node, corrupts the first page of the named relation, and
 | |
| # restarts the node.
 | |
| sub corrupt_first_page
 | |
| {
 | |
| 	my ($relname) = @_;
 | |
| 	my $relpath = relation_filepath($relname);
 | |
| 
 | |
| 	$node->stop;
 | |
| 
 | |
| 	my $fh;
 | |
| 	open($fh, '+<', $relpath)
 | |
| 	  or BAIL_OUT("open failed: $!");
 | |
| 	binmode $fh;
 | |
| 
 | |
| 	# Corrupt some line pointers.  The values are chosen to hit the
 | |
| 	# various line-pointer-corruption checks in verify_heapam.c
 | |
| 	# on both little-endian and big-endian architectures.
 | |
| 	sysseek($fh, 32, 0)
 | |
| 	  or BAIL_OUT("sysseek failed: $!");
 | |
| 	syswrite(
 | |
| 		$fh,
 | |
| 		pack("L*",
 | |
| 			0xAAA15550, 0xAAA0D550, 0x00010000,
 | |
| 			0x00008000, 0x0000800F, 0x001e8000)
 | |
| 	) or BAIL_OUT("syswrite failed: $!");
 | |
| 	close($fh)
 | |
| 	  or BAIL_OUT("close failed: $!");
 | |
| 
 | |
| 	$node->start;
 | |
| }
 | |
| 
 | |
| sub detects_heap_corruption
 | |
| {
 | |
| 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 | |
| 
 | |
| 	my ($function, $testname) = @_;
 | |
| 
 | |
| 	detects_corruption(
 | |
| 		$function,
 | |
| 		$testname,
 | |
| 		qr/line pointer redirection to item at offset \d+ precedes minimum offset \d+/,
 | |
| 		qr/line pointer redirection to item at offset \d+ exceeds maximum offset \d+/,
 | |
| 		qr/line pointer to page offset \d+ is not maximally aligned/,
 | |
| 		qr/line pointer length \d+ is less than the minimum tuple header size \d+/,
 | |
| 		qr/line pointer to page offset \d+ with length \d+ ends beyond maximum page offset \d+/,
 | |
| 	);
 | |
| }
 | |
| 
 | |
| sub detects_corruption
 | |
| {
 | |
| 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 | |
| 
 | |
| 	my ($function, $testname, @re) = @_;
 | |
| 
 | |
| 	my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
 | |
| 	like($result, $_, $testname) for (@re);
 | |
| }
 | |
| 
 | |
| sub detects_no_corruption
 | |
| {
 | |
| 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 | |
| 
 | |
| 	my ($function, $testname) = @_;
 | |
| 
 | |
| 	my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
 | |
| 	is($result, '', $testname);
 | |
| }
 | |
| 
 | |
| # Check various options are stable (don't abort) and do not report corruption
 | |
| # when running verify_heapam on an uncorrupted test table.
 | |
| #
 | |
| # The relname *must* be an uncorrupted table, or this will fail.
 | |
| #
 | |
| # The prefix is used to identify the test, along with the options,
 | |
| # and should be unique.
 | |
| sub check_all_options_uncorrupted
 | |
| {
 | |
| 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 | |
| 
 | |
| 	my ($relname, $prefix) = @_;
 | |
| 
 | |
| 	for my $stop (qw(true false))
 | |
| 	{
 | |
| 		for my $check_toast (qw(true false))
 | |
| 		{
 | |
| 			for my $skip ("'none'", "'all-frozen'", "'all-visible'")
 | |
| 			{
 | |
| 				for my $startblock (qw(NULL 0))
 | |
| 				{
 | |
| 					for my $endblock (qw(NULL 0))
 | |
| 					{
 | |
| 						my $opts =
 | |
| 						    "on_error_stop := $stop, "
 | |
| 						  . "check_toast := $check_toast, "
 | |
| 						  . "skip := $skip, "
 | |
| 						  . "startblock := $startblock, "
 | |
| 						  . "endblock := $endblock";
 | |
| 
 | |
| 						detects_no_corruption(
 | |
| 							"verify_heapam('$relname', $opts)",
 | |
| 							"$prefix: $opts");
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| done_testing();
 |