From b00123c3742dfd3d03cf8845c371e7e667cf1b71 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Sun, 8 Nov 2026 11:17:10 +0500 Subject: [PATCH v7 1/3] amcheck: add indexallkeysmatch verification for B-Tree indexes Add an optional reverse check that index tuples agree with visible heap tuples. Fingerprint heap (key, TID) pairs in a Bloom filter before scanning the index, and fetch tuples on Bloom misses to verify their keys. Reuse a slot and executor state for these lookups and normalize both representations with the existing heapallindexed machinery. Use table_fetch_tid() to resolve HOT chains and LP_REDIRECT before fetching the visible row version. Retain the original indexed TID for comparison. Cover both HOT states with forced key mismatches, and exercise the existing varlena normalization cases with the new option enabled. Author: Andrey Borodin Author: Jakub Wartak Reviewed-by: Zsolt Parragi Reviewed-by: Wenbo Lin Discussion: https://postgr.es/m/432626F9-65DF-4F0D-B345-26CFC3E2CFAC@yandex-team.ru --- contrib/amcheck/Makefile | 4 +- contrib/amcheck/amcheck--1.5--1.6.sql | 21 ++ contrib/amcheck/amcheck.control | 2 +- contrib/amcheck/expected/check_btree.out | 26 +- contrib/amcheck/meson.build | 2 + contrib/amcheck/sql/check_btree.sql | 13 +- .../t/007_verify_nbtree_indexallkeysmatch.pl | 178 ++++++++++ contrib/amcheck/verify_nbtree.c | 332 ++++++++++++++++-- 8 files changed, 533 insertions(+), 45 deletions(-) create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql create mode 100644 contrib/amcheck/t/007_verify_nbtree_indexallkeysmatch.pl diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile index 1b7a63cbaa4..fdf5970de25 100644 --- a/contrib/amcheck/Makefile +++ b/contrib/amcheck/Makefile @@ -10,12 +10,12 @@ OBJS = \ EXTENSION = amcheck DATA = amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql \ - amcheck--1.3--1.4.sql amcheck--1.4--1.5.sql + amcheck--1.3--1.4.sql amcheck--1.4--1.5.sql amcheck--1.5--1.6.sql PGFILEDESC = "amcheck - function for verifying relation integrity" REGRESS = check check_btree check_gin check_heap -EXTRA_INSTALL = contrib/pg_walinspect +EXTRA_INSTALL = contrib/pg_walinspect contrib/pageinspect TAP_TESTS = 1 ifdef USE_PGXS diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql new file mode 100644 index 00000000000..ac01f429d7d --- /dev/null +++ b/contrib/amcheck/amcheck--1.5--1.6.sql @@ -0,0 +1,21 @@ +/* contrib/amcheck/amcheck--1.5--1.6.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.6'" to load this file. \quit + +-- Add indexallkeysmatch parameter to bt_index_check and bt_index_parent_check +CREATE FUNCTION bt_index_check(index regclass, + heapallindexed boolean, checkunique boolean, indexallkeysmatch boolean) +RETURNS VOID +AS 'MODULE_PATHNAME', 'bt_index_check' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +CREATE FUNCTION bt_index_parent_check(index regclass, + heapallindexed boolean, rootdescend boolean, checkunique boolean, + indexallkeysmatch boolean) +RETURNS VOID +AS 'MODULE_PATHNAME', 'bt_index_parent_check' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean, boolean) FROM PUBLIC; +REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean, boolean) FROM PUBLIC; diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control index c8ba6d7c9bc..2f329ef2cf4 100644 --- a/contrib/amcheck/amcheck.control +++ b/contrib/amcheck/amcheck.control @@ -1,5 +1,5 @@ # amcheck extension comment = 'functions for verifying relation integrity' -default_version = '1.5' +default_version = '1.6' module_pathname = '$libdir/amcheck' relocatable = true diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out index 4fcee5da463..9b7f2ddb9aa 100644 --- a/contrib/amcheck/expected/check_btree.out +++ b/contrib/amcheck/expected/check_btree.out @@ -179,7 +179,7 @@ WHERE attrelid = 'toast_bug'::regclass AND attname = 'buggy'; -- Insert compressible heap tuple (comfortably exceeds TOAST_TUPLE_THRESHOLD): INSERT INTO toast_bug SELECT repeat('a', 2200); -- Should not get false positive report of corruption: -SELECT bt_index_check('toasty', true); +SELECT bt_index_check('toasty', true, false, true); bt_index_check ---------------- @@ -245,6 +245,26 @@ SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend (1 row) +-- indexallkeysmatch: verify each index tuple points to heap tuple with same key +SELECT bt_index_check('bttest_a_idx', heapallindexed => false, checkunique => false, indexallkeysmatch => true); + bt_index_check +---------------- + +(1 row) + +SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => false, rootdescend => false, checkunique => false, indexallkeysmatch => true); + bt_index_parent_check +----------------------- + +(1 row) + +-- indexallkeysmatch on expression index (exercises FormIndexDatum/ii_ExpressionsState path) +SELECT bt_index_check('bttest_a_expr_idx', heapallindexed => false, checkunique => false, indexallkeysmatch => true); + bt_index_check +---------------- + +(1 row) + -- Check that null values in an unique index are not treated as equal CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE); INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default); @@ -267,7 +287,7 @@ ALTER TABLE varlena_bug ALTER column v SET storage plain; INSERT INTO varlena_bug VALUES ('x'); COPY varlena_bug from stdin; CREATE INDEX varlena_bug_idx on varlena_bug(v); -SELECT bt_index_check('varlena_bug_idx', true); +SELECT bt_index_check('varlena_bug_idx', true, false, true); bt_index_check ---------------- @@ -277,7 +297,7 @@ SELECT bt_index_check('varlena_bug_idx', true); -- uncompressed in index. INSERT INTO varlena_bug VALUES (repeat('Test', 250)); ALTER TABLE varlena_bug ALTER COLUMN v SET STORAGE extended; -SELECT bt_index_check('varlena_bug_idx', true); +SELECT bt_index_check('varlena_bug_idx', true, false, true); bt_index_check ---------------- diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build index d5137ef691d..220b1ce1d59 100644 --- a/contrib/amcheck/meson.build +++ b/contrib/amcheck/meson.build @@ -27,6 +27,7 @@ install_data( 'amcheck--1.2--1.3.sql', 'amcheck--1.3--1.4.sql', 'amcheck--1.4--1.5.sql', + 'amcheck--1.5--1.6.sql', kwargs: contrib_data_args, ) @@ -50,6 +51,7 @@ tests += { 't/004_verify_nbtree_unique.pl', 't/005_pitr.pl', 't/006_verify_gin.pl', + 't/007_verify_nbtree_indexallkeysmatch.pl', ], }, } diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql index a5585085fad..91468b973d6 100644 --- a/contrib/amcheck/sql/check_btree.sql +++ b/contrib/amcheck/sql/check_btree.sql @@ -120,7 +120,7 @@ WHERE attrelid = 'toast_bug'::regclass AND attname = 'buggy'; -- Insert compressible heap tuple (comfortably exceeds TOAST_TUPLE_THRESHOLD): INSERT INTO toast_bug SELECT repeat('a', 2200); -- Should not get false positive report of corruption: -SELECT bt_index_check('toasty', true); +SELECT bt_index_check('toasty', true, false, true); -- -- Check that index expressions and predicates are run as the table's owner, @@ -160,6 +160,13 @@ SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => tr SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true); SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true); +-- indexallkeysmatch: verify each index tuple points to heap tuple with same key +SELECT bt_index_check('bttest_a_idx', heapallindexed => false, checkunique => false, indexallkeysmatch => true); +SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => false, rootdescend => false, checkunique => false, indexallkeysmatch => true); + +-- indexallkeysmatch on expression index (exercises FormIndexDatum/ii_ExpressionsState path) +SELECT bt_index_check('bttest_a_expr_idx', heapallindexed => false, checkunique => false, indexallkeysmatch => true); + -- Check that null values in an unique index are not treated as equal CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE); INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default); @@ -175,13 +182,13 @@ COPY varlena_bug from stdin; x \. CREATE INDEX varlena_bug_idx on varlena_bug(v); -SELECT bt_index_check('varlena_bug_idx', true); +SELECT bt_index_check('varlena_bug_idx', true, false, true); -- Also check that we compress varlena values, which were previously stored -- uncompressed in index. INSERT INTO varlena_bug VALUES (repeat('Test', 250)); ALTER TABLE varlena_bug ALTER COLUMN v SET STORAGE extended; -SELECT bt_index_check('varlena_bug_idx', true); +SELECT bt_index_check('varlena_bug_idx', true, false, true); -- cleanup DROP TABLE bttest_a; diff --git a/contrib/amcheck/t/007_verify_nbtree_indexallkeysmatch.pl b/contrib/amcheck/t/007_verify_nbtree_indexallkeysmatch.pl new file mode 100644 index 00000000000..349a85befec --- /dev/null +++ b/contrib/amcheck/t/007_verify_nbtree_indexallkeysmatch.pl @@ -0,0 +1,178 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Test indexallkeysmatch verification: each index tuple must point to a heap +# tuple with the same key. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $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; + CREATE EXTENSION pageinspect; +)); + +# +# Test 1: indexallkeysmatch on uncorrupted table (plain column) +# +$node->safe_psql('postgres', q( + CREATE TABLE idxall_plain (id int); + INSERT INTO idxall_plain SELECT generate_series(1, 1000); + CREATE INDEX idxall_plain_idx ON idxall_plain (id); +)); + +my $result = $node->safe_psql('postgres', + q(SELECT bt_index_check('idxall_plain_idx', false, false, true))); +is($result, '', 'indexallkeysmatch passes on uncorrupted plain table'); + +# +# Test 2: indexallkeysmatch on uncorrupted table with deduplication +# +$node->safe_psql('postgres', q( + CREATE TABLE idxall_dedup (id int); + INSERT INTO idxall_dedup SELECT i % 10 FROM generate_series(1, 1000) i; + CREATE INDEX idxall_dedup_idx ON idxall_dedup (id) + WITH (deduplicate_items = on); +)); + +$result = $node->safe_psql('postgres', + q(SELECT bt_index_check('idxall_dedup_idx', false, false, true))); +is($result, '', 'indexallkeysmatch passes on uncorrupted table with deduplication'); + +# +# Test 3: indexallkeysmatch on uncorrupted expression index +# +$node->safe_psql('postgres', q( + CREATE FUNCTION idxall_func(int) RETURNS int LANGUAGE sql IMMUTABLE AS + $$ SELECT $1; $$; + + CREATE TABLE idxall_expr (id int); + INSERT INTO idxall_expr SELECT generate_series(1, 500); + CREATE INDEX idxall_expr_idx ON idxall_expr (idxall_func(id)); +)); + +$result = $node->safe_psql('postgres', + q(SELECT bt_index_check('idxall_expr_idx', false, false, true))); +is($result, '', 'indexallkeysmatch passes on uncorrupted expression index'); + +# +# Test 4: Detect corruption -- swap expression function so heap re-evaluation +# produces different keys than what the index stores. +# +# The index was built with idxall_func(x) = x. Now change it to return x+1. +# This simulates the corruption scenario: index says key=42 for a TID, but +# re-reading the heap and re-evaluating gives key=43. +# +$node->safe_psql('postgres', q( + CREATE OR REPLACE FUNCTION idxall_func(int) RETURNS int LANGUAGE sql IMMUTABLE AS + $$ SELECT $1 + 1; $$; +)); + +my ($stdout, $stderr); +($result, $stdout, $stderr) = $node->psql('postgres', + q(SELECT bt_index_check('idxall_expr_idx', false, false, true))); +like($stderr, + qr/index tuple in index "idxall_expr_idx" does not match heap tuple/, + 'detected index-heap key mismatch via expression function swap'); + +# +# Test 5: Restore function and verify no corruption reported +# +$node->safe_psql('postgres', q( + CREATE OR REPLACE FUNCTION idxall_func(int) RETURNS int LANGUAGE sql IMMUTABLE AS + $$ SELECT $1; $$; +)); + +$result = $node->safe_psql('postgres', + q(SELECT bt_index_check('idxall_expr_idx', false, false, true))); +is($result, '', 'indexallkeysmatch passes after restoring correct function'); + +# +# Test 6: indexallkeysmatch with bt_index_parent_check +# +$node->safe_psql('postgres', q( + CREATE OR REPLACE FUNCTION idxall_func(int) RETURNS int LANGUAGE sql IMMUTABLE AS + $$ SELECT $1 + 1; $$; +)); + +($result, $stdout, $stderr) = $node->psql('postgres', + q(SELECT bt_index_parent_check('idxall_expr_idx', false, false, false, true))); +like($stderr, + qr/index tuple in index "idxall_expr_idx" does not match heap tuple/, + 'bt_index_parent_check also detects index-heap key mismatch'); + +# A HOT update leaves the index pointing to the root, not the visible tuple. +# Hold an old snapshot to prevent pruning, then release it and VACUUM to +# exercise LP_REDIRECT as well. Changing the expression forces Bloom misses: +# a successful check of an uncorrupted index alone need not fetch any tuples. +$node->safe_psql('postgres', q( + CREATE FUNCTION idxall_hot_func(int) RETURNS int LANGUAGE sql IMMUTABLE AS + $$ SELECT $1; $$; + CREATE TABLE idxall_hot (id int, payload int) WITH (fillfactor = 10); + INSERT INTO idxall_hot SELECT i, 0 FROM generate_series(1, 10) i; + CREATE INDEX idxall_hot_idx ON idxall_hot (idxall_hot_func(id)); +)); +my $holder = $node->background_psql('postgres'); +$holder->query_safe(q( + BEGIN ISOLATION LEVEL REPEATABLE READ; + SELECT count(*) FROM idxall_hot; +)); +$node->safe_psql('postgres', 'UPDATE idxall_hot SET payload = 1'); +is($node->safe_psql('postgres', q( + SELECT count(*) FROM heap_page_items(get_raw_page('idxall_hot', 0)) + WHERE lp_flags = 1 AND (t_infomask2 & 16384) <> 0; +)), '10', 'HOT roots remain normal line pointers before pruning'); +check_hot_chain('unpruned HOT chain'); + +$holder->query_safe('COMMIT'); +$holder->quit; +$node->safe_psql('postgres', 'VACUUM idxall_hot'); +is($node->safe_psql('postgres', q( + SELECT count(*) FROM heap_page_items(get_raw_page('idxall_hot', 0)) + WHERE lp_flags = 2; +)), '10', 'VACUUM converted HOT roots to LP_REDIRECT'); +check_hot_chain('LP_REDIRECT'); + +$node->stop; +done_testing(); + +sub check_hot_chain +{ + my ($description) = @_; + + for my $function ('bt_index_check', 'bt_index_parent_check') + { + my $extra = $function eq 'bt_index_check' ? 'false' : 'false, false'; + is($node->safe_psql('postgres', + qq(SELECT $function('idxall_hot_idx', true, $extra, true))), + '', "$function accepts $description"); + } + + $node->safe_psql('postgres', q( + CREATE OR REPLACE FUNCTION idxall_hot_func(int) RETURNS int + LANGUAGE sql IMMUTABLE AS $$ SELECT $1 + 1; $$; + )); + for my $function ('bt_index_check', 'bt_index_parent_check') + { + my $extra = $function eq 'bt_index_check' ? 'false' : 'false, false'; + my ($ret, $out, $err) = $node->psql('postgres', + qq(SELECT $function('idxall_hot_idx', false, $extra, true))); + is($ret, 3, "$function rejects mismatched key behind $description"); + like($err, + qr/index tuple in index "idxall_hot_idx" does not match heap tuple/, + "$function follows $description to compare keys"); + } + $node->safe_psql('postgres', q( + CREATE OR REPLACE FUNCTION idxall_hot_func(int) RETURNS int + LANGUAGE sql IMMUTABLE AS $$ SELECT $1; $$; + )); +} diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index 97c519b47f5..94e43a23e52 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -13,6 +13,11 @@ * verify its structure. A heap scan later uses Bloom filter probes to verify * that every visible heap tuple has a matching index tuple. * + * When index-to-heap verification (indexallkeysmatch) is requested, a Bloom + * filter fingerprints (key,tid) from a heap scan first. The index scan then + * probes this filter; when the probe fails, a heap lookup verifies that the + * index tuple points to a heap tuple with the same key. + * * * Copyright (c) 2017-2026, PostgreSQL Global Development Group * @@ -30,10 +35,10 @@ #include "access/tableam.h" #include "access/transam.h" #include "access/xact.h" -#include "verify_common.h" #include "catalog/index.h" #include "catalog/pg_am.h" #include "common/pg_prng.h" +#include "executor/executor.h" #include "lib/bloomfilter.h" #include "miscadmin.h" #include "storage/smgr.h" @@ -41,6 +46,7 @@ #include "utils/memutils.h" #include "utils/snapmgr.h" +#include "verify_common.h" PG_MODULE_MAGIC_EXT( .name = "amcheck", @@ -81,6 +87,8 @@ typedef struct BtreeCheckState bool readonly; /* Also verifying heap has no unindexed tuples? */ bool heapallindexed; + /* Also verifying each index tuple points to heap tuple with same key? */ + bool indexallkeysmatch; /* Also making sure non-pivot tuples can be found by new search? */ bool rootdescend; /* Also check uniqueness constraint if index is unique */ @@ -131,6 +139,18 @@ typedef struct BtreeCheckState bloom_filter *filter; /* Debug counter */ int64 heaptuplespresent; + + /* + * Mutable state, for optional indexallkeysmatch verification: + */ + + /* Bloom filter fingerprints heap (key,tid) pairs */ + bloom_filter *heapfilter; + /* Debug counter for index tuples verified */ + int64 indextuplesverified; + /* Reusable slot and executor state for FormIndexDatum() */ + TupleTableSlot *iakm_slot; + EState *iakm_estate; } BtreeCheckState; /* @@ -168,6 +188,7 @@ typedef struct BTCallbackState { bool parentcheck; bool heapallindexed; + bool indexallkeysmatch; bool rootdescend; bool checkunique; } BTCallbackState; @@ -179,7 +200,7 @@ static void bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool readonly); static void bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, bool readonly, bool heapallindexed, - bool rootdescend, bool checkunique); + bool indexallkeysmatch, bool rootdescend, bool checkunique); static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level); static bool bt_leftmost_ignoring_half_dead(BtreeCheckState *state, @@ -211,6 +232,13 @@ static void bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit, static void bt_tuple_present_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *checkstate); +static void bt_heap_fingerprint_callback(Relation index, ItemPointer tid, + Datum *values, bool *isnull, + bool tupleIsAlive, void *checkstate); +static void bt_verify_index_tuple_points_to_heap(BtreeCheckState *state, + IndexTuple itup, + BlockNumber targetblock, + OffsetNumber offset); static IndexTuple bt_normalize_tuple(BtreeCheckState *state, IndexTuple itup); static inline IndexTuple bt_posting_plain_tuple(IndexTuple itup, int n); @@ -239,13 +267,15 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state, static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup); /* - * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) + * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean, indexallkeysmatch boolean) * * Verify integrity of B-Tree index. * * Acquires AccessShareLock on heap & index relations. Does not consider * invariants that exist between parent/child pages. Optionally verifies - * that heap does not contain any unindexed or incorrectly indexed tuples. + * that heap does not contain any unindexed or incorrectly indexed tuples + * (heapallindexed), or that each index tuple points to a heap tuple with + * the same key (indexallkeysmatch). */ Datum bt_index_check(PG_FUNCTION_ARGS) @@ -254,6 +284,7 @@ bt_index_check(PG_FUNCTION_ARGS) BTCallbackState args; args.heapallindexed = false; + args.indexallkeysmatch = false; args.rootdescend = false; args.parentcheck = false; args.checkunique = false; @@ -262,6 +293,8 @@ bt_index_check(PG_FUNCTION_ARGS) args.heapallindexed = PG_GETARG_BOOL(1); if (PG_NARGS() >= 3) args.checkunique = PG_GETARG_BOOL(2); + if (PG_NARGS() >= 4) + args.indexallkeysmatch = PG_GETARG_BOOL(3); amcheck_lock_relation_and_check(indrelid, BTREE_AM_OID, bt_index_check_callback, @@ -271,13 +304,15 @@ bt_index_check(PG_FUNCTION_ARGS) } /* - * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) + * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean, indexallkeysmatch boolean) * * Verify integrity of B-Tree index. * * Acquires ShareLock on heap & index relations. Verifies that downlinks in * parent pages are valid lower bounds on child pages. Optionally verifies - * that heap does not contain any unindexed or incorrectly indexed tuples. + * that heap does not contain any unindexed or incorrectly indexed tuples + * (heapallindexed), or that each index tuple points to a heap tuple with + * the same key (indexallkeysmatch). */ Datum bt_index_parent_check(PG_FUNCTION_ARGS) @@ -286,6 +321,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS) BTCallbackState args; args.heapallindexed = false; + args.indexallkeysmatch = false; args.rootdescend = false; args.parentcheck = true; args.checkunique = false; @@ -296,6 +332,8 @@ bt_index_parent_check(PG_FUNCTION_ARGS) args.rootdescend = PG_GETARG_BOOL(2); if (PG_NARGS() >= 4) args.checkunique = PG_GETARG_BOOL(3); + if (PG_NARGS() >= 5) + args.indexallkeysmatch = PG_GETARG_BOOL(4); amcheck_lock_relation_and_check(indrelid, BTREE_AM_OID, bt_index_check_callback, @@ -335,7 +373,8 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea /* Check index, possibly against table it is an index on */ bt_check_every_level(indrel, heaprel, heapkeyspace, readonly, - args->heapallindexed, args->rootdescend, args->checkunique); + args->heapallindexed, args->indexallkeysmatch, + args->rootdescend, args->checkunique); } /* @@ -363,8 +402,8 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea */ static void bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, - bool readonly, bool heapallindexed, bool rootdescend, - bool checkunique) + bool readonly, bool heapallindexed, bool indexallkeysmatch, + bool rootdescend, bool checkunique) { BtreeCheckState *state; Page metapage; @@ -394,10 +433,41 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, state->heapkeyspace = heapkeyspace; state->readonly = readonly; state->heapallindexed = heapallindexed; + state->indexallkeysmatch = indexallkeysmatch; state->rootdescend = rootdescend; state->checkunique = checkunique; state->snapshot = InvalidSnapshot; + if (state->heapallindexed || state->indexallkeysmatch) + { + /* + * Register our own snapshot for heapallindexed/indexallkeysmatch, + * rather than asking table_index_build_scan() to do this for us + * later. This needs to happen before fingerprinting begins. + */ + state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); + + /* + * GetTransactionSnapshot() always acquires a new MVCC snapshot in + * READ COMMITTED mode. A new snapshot is guaranteed to have all the + * entries it requires in the index. + * + * We must defend against the possibility that an old xact snapshot + * was returned at higher isolation levels when that snapshot is not + * safe for index scans of the target index. This is possible when + * the snapshot sees tuples that are before the index's indcheckxmin + * horizon. Throwing an error here should be very rare. It doesn't + * seem worth using a secondary snapshot to avoid this. + */ + if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin && + !TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data), + state->snapshot->xmin)) + ereport(ERROR, + errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("index \"%s\" cannot be verified using transaction snapshot", + RelationGetRelationName(rel))); + } + if (state->heapallindexed) { int64 total_pages; @@ -414,40 +484,36 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, total_pages = RelationGetNumberOfBlocks(rel); total_elems = Max(total_pages * (MaxTIDsPerBTreePage / 3), (int64) state->rel->rd_rel->reltuples); - /* Generate a random seed to avoid repetition */ seed = pg_prng_uint64(&pg_global_prng_state); - /* Create Bloom filter to fingerprint index */ state->filter = bloom_create(total_elems, maintenance_work_mem, seed); state->heaptuplespresent = 0; + } + + if (state->indexallkeysmatch) + { + int64 total_pages; + int64 total_elems; + uint64 seed; /* - * Register our own snapshot for heapallindexed, rather than asking - * table_index_build_scan() to do this for us later. This needs to - * happen before index fingerprinting begins, so we can later be - * certain that index fingerprinting should have reached all tuples - * returned by table_index_build_scan(). + * Size Bloom filter based on estimated number of heap tuples. */ - state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); + total_pages = RelationGetNumberOfBlocks(heaprel); + total_elems = Max(total_pages * MaxHeapTuplesPerPage, + (int64) heaprel->rd_rel->reltuples); + seed = pg_prng_uint64(&pg_global_prng_state); + state->heapfilter = bloom_create(total_elems, maintenance_work_mem, seed); + state->indextuplesverified = 0; /* - * GetTransactionSnapshot() always acquires a new MVCC snapshot in - * READ COMMITTED mode. A new snapshot is guaranteed to have all the - * entries it requires in the index. - * - * We must defend against the possibility that an old xact snapshot - * was returned at higher isolation levels when that snapshot is not - * safe for index scans of the target index. This is possible when - * the snapshot sees tuples that are before the index's indcheckxmin - * horizon. Throwing an error here should be very rare. It doesn't - * seem worth using a secondary snapshot to avoid this. + * Set up reusable infrastructure for heap lookups by TID: a slot and + * an executor state for FormIndexDatum(). Creating these once per + * verification avoids rebuilding executor state (and recompiling + * index expressions) for every index tuple that misses the Bloom + * filter. */ - if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin && - !TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data), - state->snapshot->xmin)) - ereport(ERROR, - errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("index \"%s\" cannot be verified using transaction snapshot", - RelationGetRelationName(rel))); + state->iakm_slot = table_slot_create(heaprel, NULL); + state->iakm_estate = CreateExecutorState(); } /* @@ -455,11 +521,12 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, * performance, take it once per index check. If one was already taken * above, use that. */ - if (state->checkunique) + if (state->checkunique || state->indexallkeysmatch) { state->indexinfo = BuildIndexInfo(state->rel); - if (state->indexinfo->ii_Unique && state->snapshot == InvalidSnapshot) + if (state->checkunique && state->indexinfo->ii_Unique && + state->snapshot == InvalidSnapshot) state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); } @@ -477,6 +544,37 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, ALLOCSET_DEFAULT_SIZES); state->checkstrategy = GetAccessStrategy(BAS_BULKREAD); + /* + * When indexallkeysmatch, fingerprint heap first so we can verify each + * index tuple points to a heap tuple with the same key during the index + * scan. + */ + if (state->indexallkeysmatch) + { + IndexInfo *indexinfo = BuildIndexInfo(state->rel); + TableScanDesc scan; + + scan = table_beginscan_strat(state->heaprel, + state->snapshot, + 0, NULL, true, true); + indexinfo->ii_Concurrent = true; + indexinfo->ii_Unique = false; + indexinfo->ii_ExclusionOps = NULL; + indexinfo->ii_ExclusionProcs = NULL; + indexinfo->ii_ExclusionStrats = NULL; + + elog(DEBUG1, "fingerprinting heap \"%s\" for index \"%s\" verification", + RelationGetRelationName(state->heaprel), + RelationGetRelationName(state->rel)); + + table_index_build_scan(state->heaprel, state->rel, indexinfo, true, false, + bt_heap_fingerprint_callback, state, scan); + + ereport(DEBUG1, + (errmsg_internal("finished heap fingerprint with bitset %.2f%% set", + 100.0 * bloom_prop_bits_set(state->heapfilter)))); + } + /* Get true root block from meta-page */ metapage = palloc_btree_page(state, BTREE_METAPAGE); metad = BTPageGetMeta(metapage); @@ -583,6 +681,19 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, bloom_free(state->filter); } + if (state->indexallkeysmatch) + { + ereport(DEBUG1, + (errmsg_internal("finished verifying " INT64_FORMAT " index tuples point to matching heap tuples", + state->indextuplesverified))); + bloom_free(state->heapfilter); + ExecDropSingleTupleTableSlot(state->iakm_slot); + FreeExecutorState(state->iakm_estate); + /* These may have been pointing to the now-gone estate */ + state->indexinfo->ii_ExpressionsState = NIL; + state->indexinfo->ii_PredicateState = NULL; + } + /* Be tidy: */ if (state->snapshot != InvalidSnapshot) UnregisterSnapshot(state->snapshot); @@ -1503,6 +1614,30 @@ bt_target_page_check(BtreeCheckState *state) } } + /* Verify each index tuple points to heap tuple with same key */ + if (state->indexallkeysmatch && P_ISLEAF(topaque) && !ItemIdIsDead(itemid)) + { + if (BTreeTupleIsPosting(itup)) + { + int i; + + for (i = 0; i < BTreeTupleGetNPosting(itup); i++) + { + IndexTuple logtuple; + + logtuple = bt_posting_plain_tuple(itup, i); + bt_verify_index_tuple_points_to_heap(state, logtuple, + state->targetblock, offset); + pfree(logtuple); + } + } + else + { + bt_verify_index_tuple_points_to_heap(state, itup, + state->targetblock, offset); + } + } + /* * * High key check * * @@ -2801,6 +2936,131 @@ bt_tuple_present_callback(Relation index, ItemPointer tid, Datum *values, pfree(norm); } +/* + * Per-tuple callback from table_index_build_scan for indexallkeysmatch. Add + * each visible heap tuple's (key, tid) to the Bloom filter for later probe + * during the index scan. + */ +static void +bt_heap_fingerprint_callback(Relation index, ItemPointer tid, Datum *values, + bool *isnull, bool tupleIsAlive, void *checkstate) +{ + BtreeCheckState *state = (BtreeCheckState *) checkstate; + IndexTuple itup, + norm; + + Assert(state->indexallkeysmatch); + + itup = index_form_tuple(RelationGetDescr(index), values, isnull); + itup->t_tid = *tid; + norm = bt_normalize_tuple(state, itup); + bloom_add_element(state->heapfilter, (unsigned char *) norm, + IndexTupleSize(norm)); + pfree(itup); + if (norm != itup) + pfree(norm); +} + +/* + * Launched only with indexallkeysmatch: + * Verify that the index tuple points to a heap tuple with the same key. + * When the Bloom filter lacks the (key, tid), fetch the heap tuple and + * compare keys. + * + * The heap lookup mirrors what a regular index scan does: + * table_fetch_tid() with our MVCC snapshot follows LP_REDIRECT and HOT + * chains, resolving the TID of the visible tuple version, which we then + * load into a slot with table_tuple_fetch_row_version(). A "not found" + * result means the tuple is dead or was concurrently pruned; we skip it + * exactly as an index scan would. Here we verify that the visible + * heap tuple matches the index key. + */ +static void +bt_verify_index_tuple_points_to_heap(BtreeCheckState *state, IndexTuple itup, + BlockNumber targetblock, OffsetNumber offset) +{ + ItemPointer tid = BTreeTupleGetHeapTID(itup); + IndexTuple norm; + bool in_filter; + + Assert(state->indexallkeysmatch); + + norm = bt_normalize_tuple(state, itup); + in_filter = !bloom_lacks_element(state->heapfilter, (unsigned char *) norm, + IndexTupleSize(norm)); + if (norm != itup) + pfree(norm); + + if (in_filter) + { + /* Probably present in the heap fingerprint */ + state->indextuplesverified++; + return; + } + + /* + * Bloom filter says (key, tid) not in heap. Follow TID to verify; this + * amortizes random heap lookups by only fetching when the probe indicates + * absence (Bloom filters have false positives, never false negatives, so + * "not in" means we must verify), or reports corruption when the index + * points to a heap tuple with a different key. + */ + { + TupleTableSlot *slot = state->iakm_slot; + IndexTuple heap_itup; + IndexTuple heap_norm; + Datum values[INDEX_MAX_KEYS]; + bool isnull[INDEX_MAX_KEYS]; + ItemPointerData visibletid; + + /* Reclaim memory from the previous FormIndexDatum() call, if any */ + ResetPerTupleExprContext(state->iakm_estate); + + /* + * MVCC snapshot holds back the horizon for any tuple it can see, so + * the fetch of the resolved TID should also succeed. + */ + visibletid = *tid; + if (!table_fetch_tid(state->heaprel, &visibletid, state->snapshot, NULL)) + return; + + if (!table_tuple_fetch_row_version(state->heaprel, &visibletid, + state->snapshot, slot)) + elog(ERROR, "could not fetch visible tuple (%u,%u) from table \"%s\"", + ItemPointerGetBlockNumber(&visibletid), + ItemPointerGetOffsetNumber(&visibletid), + RelationGetRelationName(state->heaprel)); + + GetPerTupleExprContext(state->iakm_estate)->ecxt_scantuple = slot; + FormIndexDatum(state->indexinfo, slot, state->iakm_estate, + values, isnull); + + heap_itup = index_form_tuple(RelationGetDescr(state->rel), values, isnull); + heap_itup->t_tid = *tid; + heap_norm = bt_normalize_tuple(state, heap_itup); + + norm = bt_normalize_tuple(state, itup); + if (IndexTupleSize(heap_norm) != IndexTupleSize(norm) || + memcmp(heap_norm, norm, IndexTupleSize(norm)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index tuple in index \"%s\" does not match heap tuple in table \"%s\"", + RelationGetRelationName(state->rel), + RelationGetRelationName(state->heaprel)), + errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) with different key.", + targetblock, offset, + ItemPointerGetBlockNumber(tid), + ItemPointerGetOffsetNumber(tid)))); + + pfree(heap_itup); + if (heap_norm != heap_itup) + pfree(heap_norm); + if (norm != itup) + pfree(norm); + state->indextuplesverified++; + } +} + /* * Normalize an index tuple for fingerprinting. * base-commit: 0607aa61090ac1b574f8bafe854c684195b31e16 -- 2.50.1 (Apple Git-155)