amcheck support for BRIN indexes

Started by Arseniy Mukhin9 months ago22 messages
#1Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
2 attachment(s)

Hi,

It's not for v18, just wanted to share with the community and register
it in the upcoming Commitfest 2025-07.

Here is the patch with amcheck support for BRIN indexes.

Patch uses amcheck common infrastructure that was introduced in [1]/messages/by-id/45AC9B0A-2B45-40EE-B08F-BDCF5739D1E1@yandex-team.ru.
It works and I deleted all the code that
I copied from btree check at first. Great.

During the check we hold ShareUpdateExclusiveLock, so we don't block
regular reads/inserts/updates
and the same time range summarizations/desummarizations are impossible
during the check which simplifies check logic.
While checking we do ereport(ERROR) on the first issue, the same way
as btree, gin checks do.

There are two parts:

First part is 'index structure check':
1) Meta page check
2) Revmap check. Walk revmap and check every valid revmap item points
to the index tuple with the expected range blkno,
and index tuple is consistent with the tuple description. Also it's
not documented, but known from the brin code that
for every empty range we should have allnulls = true, hasnulls =
false. So this is also checked here.
3) Regular pages check. Walk regular pages and check that every index
tuple has a corresponding revmap item that points to it.
We don't check index tuple structures here, because it was already
done in 2 steps.

Regular pages check is optional. Orphan index tuple errors in this
check doesn't necessary mean that index is corrupted,
but AFAIS brin is not supposed to have such orphan index tuples now,
so if we encounter one than probably something
wrong with the index.

Second part is 'all consistent check':
We check all heap tuples are consistent with the index. It's the most
expensive part and it's optional.
Here we call consistent functions for every heap tuple. Also here we
check that fields like 'has_nulls', 'all_nulls',
'empty_range' are consistent with what we see in the heap.

There are two patch files:

0001-brin-refactoring.patch

It's just two tiny changes in the brin code.
1) For index tuple structure check we need to know how on-disk index
tuples look like.
Function that lets you get it 'brtuple_disk_tupdesc' is internal. This
patch makes it extern.
2) Create macros for BRIN_MAX_PAGES_PER_RANGE. For the meta page check.

0002-amechek-brin-support.patch

It's check functionality itself + regression tests + amcheck extension updates.

Some open questions:

1) How to get the correct strategy number for the scan key in "all
heap consistent" check. The consistent function wants
a scan key, and to generate it we have to pick a strategy number. We
can't just use the same strategy number for all
indexes because its meaning differs from opclass to opclass (for
example equal strategy number is 1 in Bloom
and 3 in min_max). We also can't pick an operator and use it for every
check, because opclasses don't have any requirements
about what operators they should support. The solution was to let user
to define check operator
(parameter consistent_operator_names). It's an array as we can have a
multicolumn index. We can use '=' as default check
operator, because AFAIS '=' operator is supported by all core
opclasses except 'box_inclusion_ops', and IMHO it's the
most obvious candidate for such a check. So if param
'consistent_operator_names' is an empty array (param default value),
then for all attributes we use operator '='. In most cases operator
'=' does the job and you don't need to worry about
consistent_operator_names param. Not sure about it, what do you think?

2) The current implementation of "all heap consistent" uses the scan
of the entire heap. So if we have a lot of
unsummarized ranges, we do a lot of wasted work because we can't use
the tuples that belong to the unsummarized ranges.
Instead of having one scan for the entire heap, we can walk the
revmap, take only the summarized ranges, and
scan only the pages that belong to those ranges. So we have one scan
per range. This approach helps us avoid touching
those heap tuples that we can't use for index checks. But I'm not sure
if we should to worry about that because every
autovacuum summarizes all the unsummarized ranges, so don't expect a
lot of unsummarized ranges on average.

TODO list:

1) add TAP tests
2) update SGML docs for amcheck (think it's better to do after patch
is reviewed and more or less finalized)
3) pg_amcheck integration

Big thanks to Tomas Vondra for the first patch idea and initial review.

[1]: /messages/by-id/45AC9B0A-2B45-40EE-B08F-BDCF5739D1E1@yandex-team.ru

Best regards,
Arseniy Mukhin

Attachments:

0002-amcheck-brin-support.patchtext/x-patch; charset=US-ASCII; name=0002-amcheck-brin-support.patchDownload
From 7de60e8da824a97327a5c28ccd9d8a384c07b997 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Apr 2025 11:00:36 +0300
Subject: [PATCH 2/2] amcheck - brin support

---
 contrib/amcheck/Makefile                |    5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |   20 +
 contrib/amcheck/amcheck.control         |    2 +-
 contrib/amcheck/expected/check_brin.out |  134 +++
 contrib/amcheck/meson.build             |    3 +
 contrib/amcheck/sql/check_brin.sql      |  102 ++
 contrib/amcheck/verify_brinam.c         | 1270 +++++++++++++++++++++++
 7 files changed, 1533 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/verify_brinam.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..10cba6c8feb 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brinam.o
 
 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
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..0c850a97d16
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,20 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false,
+                                 heap_all_consistent boolean default false,
+                                 consistent_operator_names text[] default '{}'
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..2690d629723
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789bcdfghjkmnpqrstvwxyz', ceil(random() * 30)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index b33e8c9b062..15c4d17a9bc 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brinam.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..36e091a6884
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789bcdfghjkmnpqrstvwxyz', ceil(random() * 30)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brinam.c b/contrib/amcheck/verify_brinam.c
new file mode 100644
index 00000000000..d01b3a2ad64
--- /dev/null
+++ b/contrib/amcheck/verify_brinam.c
@@ -0,0 +1,1270 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brinam.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2024, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brinam.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+	bool		heap_all_consistent;
+	ArrayType  *heap_all_consistent_oper_names;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+	/* All heap consistent check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void check_all_heap_consistent(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
+static void brin_check_ereport(const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->heap_all_consistent = PG_GETARG_BOOL(2);
+	state->heap_all_consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heap_all_consistent)
+	{
+		check_and_prepare_operator_names(state);
+	}
+
+	check_brin_index_structure(state);
+
+	if (state->heap_all_consistent)
+	{
+		check_all_heap_consistent(state);
+	}
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport("metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+	/* Walk each revmap page */
+	for (state->revmapBlk = BRIN_METAPAGE_BLKNO + 1; state->revmapBlk <= lastRevmapPage; state->revmapBlk++)
+	{
+
+		state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+											  state->checkstrategy);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(psprintf("revmap page is expected at block %u, last revmap page %u",
+										state->revmapBlk,
+										lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG1, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG1, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG1, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	Size		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	Size		hoff = BrinTupleDataOffset(tuple);
+	Size		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %lu is greater than tuple len %lu", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %u "
+											 "starts at offset %lu beyond total tuple length %lu",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %u "
+											 "ends at offset %lu beyond total tuple length %lu",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+	for (state->regpageBlk = state->lastRevmapPage + 1; state->regpageBlk < state->idxnblocks; state->regpageBlk++)
+	{
+		OffsetNumber maxoff;
+
+		state->regpagebuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+											   state->checkstrategy);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumber(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumber(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdGetLength(itemid) != 0) ||
+		  (!ItemIdIsUsed(itemid) && ItemIdGetLength(itemid) == 0)))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Also check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_all_heap_consistent(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* All heap consistent check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG1, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG1, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->heap_all_consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->heap_all_consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare equals ScanKey for index attribute.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ * Operator strategy number can vary from opclass to opclass, so we need to lookup it for every attribute.
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = attr->atttypid;
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
+
+/* Report without any additional info */
+static void
+brin_check_ereport(const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("index is corrupted - %s", fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("index is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("index is corrupted - %s. Index tuple: (%u,%u)",
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("index is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("index is corrupted - %s. Range blkno: %u, heap tid (%u,%u)",
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=0001-brin-refactoring.patchDownload
From 89ca3fb9ed992831de60f0bcc2246e28aa89a5ac Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH 1/2] brin refactoring

---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..72703ba150d 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

#2Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#1)
2 attachment(s)
Re: amcheck support for BRIN indexes

Hi,

Here is a new version.

TAP tests were added. Tried to reproduce more or less every violation.
I don't include 2 tests where disk representation ItemIdData needs to
be changed: it works locally, but I don't think these tests are
portable. While writing tests some minor issues were found and fixed.
Also ci compiler warnings were fixed.

Best regards,
Arseniy Mukhin

Attachments:

v2-0001-brin-refactoring.patchapplication/x-patch; name=v2-0001-brin-refactoring.patchDownload
From 918cc8b07eb19f74646949cc302d6ed67f1bc920 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v2 1/2] brin refactoring

---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v2-0002-amcheck-brin-support.patchapplication/x-patch; name=v2-0002-amcheck-brin-support.patchDownload
From 9ea0db2fb85645fa11bbc864ddf020fe2ebabe9b Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Apr 2025 11:00:36 +0300
Subject: [PATCH v2 2/2] amcheck - brin support

---
 contrib/amcheck/Makefile                |    5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |   20 +
 contrib/amcheck/amcheck.control         |    2 +-
 contrib/amcheck/expected/check_brin.out |  134 +++
 contrib/amcheck/meson.build             |    4 +
 contrib/amcheck/sql/check_brin.sql      |  102 ++
 contrib/amcheck/t/006_verify_brin.pl    |  336 ++++++
 contrib/amcheck/verify_brin.c           | 1274 +++++++++++++++++++++++
 8 files changed, 1874 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/006_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..0c850a97d16
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,20 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false,
+                                 heap_all_consistent boolean default false,
+                                 consistent_operator_names text[] default '{}'
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..2690d629723
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789bcdfghjkmnpqrstvwxyz', ceil(random() * 30)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index b33e8c9b062..bce10566525 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -49,6 +52,7 @@ tests += {
       't/003_cic_2pc.pl',
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
+      't/006_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..36e091a6884
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789bcdfghjkmnpqrstvwxyz', ceil(random() * 30)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+
+-- rebuild index
+DROP INDEX brintest_a_idx;
+CREATE INDEX brintest_a_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_a_idx'::REGCLASS, true, true, '{"@>"}');
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/006_verify_brin.pl b/contrib/amcheck/t/006_verify_brin.pl
new file mode 100644
index 00000000000..7a6ff029704
--- /dev/null
+++ b/contrib/amcheck/t/006_verify_brin.pl
@@ -0,0 +1,336 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted'
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted',
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted',
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted',
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted'
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted',
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => 'metapage is corrupted',
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => 'revmap page is expected at block 1, last revmap page 1',
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => 'revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)'
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => 'revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)'
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => 'revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)'
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => 'index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)',
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => 'the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => 'index tuple header length 31 is greater than tuple len 24. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => 'empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => 'empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => 'empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => 'attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length 24. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => 'revmap doesn\'t point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)'
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => 'range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)'
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => 'range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)'
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => 'range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)'
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => 'heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)'
+    }
+);
+
+
+# init test data
+my $i = 0;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
+    ok($stderr =~ /\Q$test_struct->{expected}\E/);
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..edd796cee59
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,1274 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2024, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+	bool		heap_all_consistent;
+	ArrayType  *heap_all_consistent_oper_names;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+	/* All heap consistent check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void check_all_heap_consistent(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->heap_all_consistent = PG_GETARG_BOOL(2);
+	state->heap_all_consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heap_all_consistent)
+	{
+		check_and_prepare_operator_names(state);
+	}
+
+	check_brin_index_structure(state);
+
+	if (state->heap_all_consistent)
+	{
+		check_all_heap_consistent(state);
+	}
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+	/* Walk each revmap page */
+	for (state->revmapBlk = BRIN_METAPAGE_BLKNO + 1; state->revmapBlk <= lastRevmapPage; state->revmapBlk++)
+	{
+
+		state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+											  state->checkstrategy);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG1, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG1, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG1, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+	for (state->regpageBlk = state->lastRevmapPage + 1; state->regpageBlk < state->idxnblocks; state->regpageBlk++)
+	{
+		OffsetNumber maxoff;
+
+		state->regpagebuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+											   state->checkstrategy);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Also check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_all_heap_consistent(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* All heap consistent check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG1, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG1, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->heap_all_consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->heap_all_consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare equals ScanKey for index attribute.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ * Operator strategy number can vary from opclass to opclass, so we need to lookup it for every attribute.
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = attr->atttypid;
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

#3Tomas Vondra
tomas@vondra.me
In reply to: Arseniy Mukhin (#2)
Re: amcheck support for BRIN indexes

On 6/8/25 14:39, Arseniy Mukhin wrote:

Hi,

Here is a new version.

TAP tests were added. Tried to reproduce more or less every violation.
I don't include 2 tests where disk representation ItemIdData needs to
be changed: it works locally, but I don't think these tests are
portable. While writing tests some minor issues were found and fixed.
Also ci compiler warnings were fixed.

Thanks. I've added myself as a reviewer, so that I don't forget about
this for the next CF.

regards

--
Tomas Vondra

#4Andrey Borodin
x4mmm@yandex-team.ru
In reply to: Arseniy Mukhin (#2)
Re: amcheck support for BRIN indexes

Hi!

Nice feature!

On 8 Jun 2025, at 17:39, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:

<v2-0001-brin-refactoring.patch>

+#define BRIN_MAX_PAGES_PER_RANGE 131072

I'd personally prefer some words on where does this limit come from. I'm not a BRIN pro, just curious.
Or, at least, maybe we can use a form 128 * 1024, if it's just a sane limit.

On 8 Jun 2025, at 17:39, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:
<v2-0002-amcheck-brin-support.patch>

A bit more detailed commit message would be very useful.

The test coverage is very decent. The number of possible corruptions in tests is impressive. I don't really have an experience with BRIN to say how different they are, but I want to ask if you are sure that these types of corruption will be portable across big-endian machines and such stuff.

Copyright year in all new files should be 2025.

Some documentation about brin_index_check() would be handy, especially about its parameters. Perhaps, somewhere near gin_index_check() in amcheck.sgml.

brin_check_ereport() reports ERRCODE_DATA_CORRUPTED. We should distinguish between ERRCODE_INDEX_CORRUPTED which is a stormbringer and ERRCODE_DATA_CORRUPTED which is an actual disaster.

If it's not very difficult - it would be great to use read_stream infrastructure. See btvacuumscan() in nbtree.c calling read_stream_begin_relation() for example. We cannot use it in logical scans in B-tree\GiST\GIN, but maybe BRIN can take some advantage of this new shiny technology.

+		state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+											  state->checkstrategy);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
// usage of state->revmapbuf
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
// more usage of state->revmapbuf
+		ReleaseBuffer(state->revmapbuf);

I hope you know what you are doing. BRIN concurrency is not known to me at all.

That's all for first pass through patches. Thanks for working on it!

Best regards, Andrey Borodin.

#5Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Andrey Borodin (#4)
3 attachment(s)
Re: amcheck support for BRIN indexes

On Mon, Jun 9, 2025 at 1:16 AM Tomas Vondra <tomas@vondra.me> wrote:

On 6/8/25 14:39, Arseniy Mukhin wrote:

Hi,

Here is a new version.

TAP tests were added. Tried to reproduce more or less every violation.
I don't include 2 tests where disk representation ItemIdData needs to
be changed: it works locally, but I don't think these tests are
portable. While writing tests some minor issues were found and fixed.
Also ci compiler warnings were fixed.

Thanks. I've added myself as a reviewer, so that I don't forget about
this for the next CF.

Thank you, looking forward to hearing your thoughts.

On Mon, Jun 16, 2025 at 8:11 PM Andrey Borodin <x4mmm@yandex-team.ru> wrote:

Hi!

Nice feature!

Hi Andrey, thank you for your interest in the patch!

On 8 Jun 2025, at 17:39, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:

<v2-0001-brin-refactoring.patch>

+#define BRIN_MAX_PAGES_PER_RANGE 131072

I'd personally prefer some words on where does this limit come from. I'm not a BRIN pro, just curious.
Or, at least, maybe we can use a form 128 * 1024, if it's just a sane limit.

Actually I don't know where this value came from, this limit already
exists in reloptions.c, so patch just creates macros so that it can be
reused in the check without duplication.

On 8 Jun 2025, at 17:39, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:
<v2-0002-amcheck-brin-support.patch>

A bit more detailed commit message would be very useful.

Agree, it was too concise. Added commit messages.

The test coverage is very decent. The number of possible corruptions in tests is impressive. I don't really have an experience with BRIN to say how different they are, but I want to ask if you are sure that these types of corruption will be portable across big-endian machines and such stuff.

Yeah, it seems that there are a lot of things that can go wrong with
the test's portability. What I think can be done to make it more
robust:
- using regular expressions to find places we want to corrupt instead
of concrete offsets. This way we avoid problems with different
alignments for 32 bit and 64 bit systems.
- using perl pack() function, so it uses the endianness of the system
where you run tests. This helps to avoid problems with different
endianness.
- don't touch things like varlena len that have different values on
different machines.
And if some test turned out to be not portable we can drop it, but at
least we would know that the code works, so it also would not be a
useless effort.

Copyright year in all new files should be 2025.

Fixed.

Some documentation about brin_index_check() would be handy, especially about its parameters. Perhaps, somewhere near gin_index_check() in amcheck.sgml.

Thanks, I'm gonna do it soon.

brin_check_ereport() reports ERRCODE_DATA_CORRUPTED. We should distinguish between ERRCODE_INDEX_CORRUPTED which is a stormbringer and ERRCODE_DATA_CORRUPTED which is an actual disaster.

Fixed. Interesting, I used btree check as reference when started
writing brin check, and in btree check there 53
ERRCODE_INDEX_CORRUPTED ereports and only 1 ERRCODE_DATA_CORRUPTED
ereport. So it was very hard to do, but I managed to pick the wrong
one. I wonder if this btree check ereport should also be changed to
ERRCODE_INDEX_CORRUPTED?

If it's not very difficult - it would be great to use read_stream infrastructure. See btvacuumscan() in nbtree.c calling read_stream_begin_relation() for example. We cannot use it in logical scans in B-tree\GiST\GIN, but maybe BRIN can take some advantage of this new shiny technology.

Thanks, I will look into it.

+               state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+                                                                                         state->checkstrategy);
+               LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
// usage of state->revmapbuf
+               LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
// more usage of state->revmapbuf
+               ReleaseBuffer(state->revmapbuf);

I hope you know what you are doing. BRIN concurrency is not known to me at all.

It seems alright, here we lock the revmap page again inside
check_revmap_item() for every revmap item.

That's all for first pass through patches. Thanks for working on it!

Thank you for the review!

Here is a new version of the patch.
It's rebased and It fixes points from Andrey's review. Two tests fail
on the 32 bit build, a new version should fix it. Also the
'all_heap_consistent' part was moved to a separate patch, it's about
400 lines of code, so I hope it is more reviewable now.
I tried the patch with postgis extension brin op classes and found a
small bug, the new version fixes it too.

Best regards,
Arseniy Mukhin

Attachments:

v3-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v3-0001-brin-refactoring.patchDownload
From 49d4d9651e9bf377ca38d3b571bf19cfc250fa65 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v3 1/3] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v3-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v3-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From 4d3277fba7dc39e2b4c7200d6a77714c07255037 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v3 2/3] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 102 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 +++++++++
 contrib/amcheck/verify_brin.c           | 807 ++++++++++++++++++++++++
 8 files changed, 1360 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..9ec046bb1cf
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..bebca93d32f
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..0a5e26ea8f5
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..7e445003ace
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,807 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2024, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+	/* Walk each revmap page */
+	for (state->revmapBlk = BRIN_METAPAGE_BLKNO + 1; state->revmapBlk <= lastRevmapPage; state->revmapBlk++)
+	{
+
+		state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+											  state->checkstrategy);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+	for (state->regpageBlk = state->lastRevmapPage + 1; state->regpageBlk < state->idxnblocks; state->regpageBlk++)
+	{
+		OffsetNumber maxoff;
+
+		state->regpagebuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+											   state->checkstrategy);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v3-0003-amcheck-brin_index_check-all-heap-consistent.patchtext/x-patch; charset=US-ASCII; name=v3-0003-amcheck-brin_index_check-all-heap-consistent.patchDownload
From 339f81129392a2444e43f7f7f291686a70a2961a Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:41:34 +0300
Subject: [PATCH v3 3/3] amcheck: brin_index_check() - all heap consistent

This commit extends functionality of brin_index_check() with
all_heap_consistent check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  18 +-
 contrib/amcheck/sql/check_brin.sql      |  18 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 482 ++++++++++++++++++++++++
 5 files changed, 554 insertions(+), 21 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 9ec046bb1cf..0c850a97d16 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regular_pages_check boolean default false
+                                 regular_pages_check boolean default false,
+                                 heap_all_consistent boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index bebca93d32f..0aa90dafa20 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0a5e26ea8f5..0f58567f76f 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -35,12 +35,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -52,12 +52,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -69,12 +69,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 7e445003ace..32a7a4c7832 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -39,6 +39,8 @@ typedef struct BrinCheckState
 	/* Check arguments */
 
 	bool		regular_pages_check;
+	bool		heap_all_consistent;
+	ArrayType  *heap_all_consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* All heap consistent check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_all_heap_consistent(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -103,6 +147,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
 	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->heap_all_consistent = PG_GETARG_BOOL(2);
+	state->heap_all_consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,21 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heap_all_consistent)
+	{
+		check_and_prepare_operator_names(state);
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heap_all_consistent)
+	{
+		check_all_heap_consistent(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -740,6 +798,414 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_all_heap_consistent(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* All heap consistent check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->heap_all_consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->heap_all_consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -805,3 +1271,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_DATA_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

#6Andrey Borodin
x4mmm@yandex-team.ru
In reply to: Arseniy Mukhin (#5)
Re: amcheck support for BRIN indexes

On 18 Jun 2025, at 11:33, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:

Interesting, I used btree check as reference when started
writing brin check, and in btree check there 53
ERRCODE_INDEX_CORRUPTED ereports and only 1 ERRCODE_DATA_CORRUPTED
ereport. So it was very hard to do, but I managed to pick the wrong
one. I wonder if this btree check ereport should also be changed to
ERRCODE_INDEX_CORRUPTED?

It's there in a case of heapallindexes failure. I concur that ERRCODE_INDEX_CORRUPTED is more appropriate in that case in verify_nbtree.c.
But I recollect Peter explained this code before somewhere in pgsql-hackers. And the reasoning was something like "if you lack a tuple in unquie constraints - it's almost certainly subsequent constrain violation and data loss". But I'm not sure.
And I could not find this discussion in archives.

Best regards, Andrey Borodin.

#7Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Andrey Borodin (#6)
3 attachment(s)
Re: amcheck support for BRIN indexes

On Wed, Jun 18, 2025 at 2:39 PM Andrey Borodin <x4mmm@yandex-team.ru> wrote:

On 18 Jun 2025, at 11:33, Arseniy Mukhin <arseniy.mukhin.dev@gmail.com> wrote:

Interesting, I used btree check as reference when started
writing brin check, and in btree check there 53
ERRCODE_INDEX_CORRUPTED ereports and only 1 ERRCODE_DATA_CORRUPTED
ereport. So it was very hard to do, but I managed to pick the wrong
one. I wonder if this btree check ereport should also be changed to
ERRCODE_INDEX_CORRUPTED?

It's there in a case of heapallindexes failure. I concur that ERRCODE_INDEX_CORRUPTED is more appropriate in that case in verify_nbtree.c.
But I recollect Peter explained this code before somewhere in pgsql-hackers. And the reasoning was something like "if you lack a tuple in unquie constraints - it's almost certainly subsequent constrain violation and data loss". But I'm not sure.
And I could not find this discussion in archives.

There is a thread about heapallindexed feature [1]/messages/by-id/CAH2-WzmVKiwcNrhYFH9CTLLcmQTMH_xjW=AvxfDKAftmY47QKw@mail.gmail.com, I guess this is a
one you mentioned? Also it turned out that this error code is
explained in the code comment:

* Since the overall structure of the index has already been verified, the most
* likely explanation for error here is a corrupt heap page (could be logical
* or physical corruption). ...

Now I wonder if we need to use ERRCODE_DATA_CORRUPTED in the 'heap all
consistent' part? It's similar to btree heapallindexed check. We have
a structurally consistent index, but for some reason it is not
consistent with the heap. It seems to me it's impossible to say who we
should blame here. I leave ERRCODE_INDEX_CORRUPTED for now.

I noticed that fixes about year and error codes didn't get to the last
version for some reason, so there is a new version with fixes. Also I
changed the 'heap all consistent' error message "Index %s is
corrupted" -> "Index %s is not consistent with the heap". New message
looks less misleading as we don't know where the problem is. Thanks!

[1]: /messages/by-id/CAH2-WzmVKiwcNrhYFH9CTLLcmQTMH_xjW=AvxfDKAftmY47QKw@mail.gmail.com

Best regards,
Arseniy Mukhin

Attachments:

v4-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v4-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From 6cb2e5f42326d079645f47873b37ccd9549bc818 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v4 2/3] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 102 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 +++++++++
 contrib/amcheck/verify_brin.c           | 807 ++++++++++++++++++++++++
 8 files changed, 1360 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..9ec046bb1cf
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..bebca93d32f
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..0a5e26ea8f5
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..9d5a1caebf6
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,807 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+	/* Walk each revmap page */
+	for (state->revmapBlk = BRIN_METAPAGE_BLKNO + 1; state->revmapBlk <= lastRevmapPage; state->revmapBlk++)
+	{
+
+		state->revmapbuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+											  state->checkstrategy);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+	for (state->regpageBlk = state->lastRevmapPage + 1; state->regpageBlk < state->idxnblocks; state->regpageBlk++)
+	{
+		OffsetNumber maxoff;
+
+		state->regpagebuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+											   state->checkstrategy);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v4-0003-amcheck-brin_index_check-heap-all-consistent.patchtext/x-patch; charset=US-ASCII; name=v4-0003-amcheck-brin_index_check-heap-all-consistent.patchDownload
From c933716493ee47742c047b6997f742dcc51d5334 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:41:34 +0300
Subject: [PATCH v4 3/3] amcheck: brin_index_check() - heap all consistent

This commit extends functionality of brin_index_check() with
heap_all_consistent check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  18 +-
 contrib/amcheck/sql/check_brin.sql      |  18 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 482 ++++++++++++++++++++++++
 5 files changed, 554 insertions(+), 21 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 9ec046bb1cf..0c850a97d16 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regular_pages_check boolean default false
+                                 regular_pages_check boolean default false,
+                                 heap_all_consistent boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index bebca93d32f..0aa90dafa20 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0a5e26ea8f5..0f58567f76f 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -35,12 +35,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -52,12 +52,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -69,12 +69,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 9d5a1caebf6..020cba50638 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -39,6 +39,8 @@ typedef struct BrinCheckState
 	/* Check arguments */
 
 	bool		regular_pages_check;
+	bool		heap_all_consistent;
+	ArrayType  *heap_all_consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all consistent check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_consistent(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -103,6 +147,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
 	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->heap_all_consistent = PG_GETARG_BOOL(2);
+	state->heap_all_consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,21 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heap_all_consistent)
+	{
+		check_and_prepare_operator_names(state);
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heap_all_consistent)
+	{
+		check_heap_all_consistent(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -740,6 +798,414 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_consistent(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* Heap all consistent check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->heap_all_consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->heap_all_consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -805,3 +1271,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

v4-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v4-0001-brin-refactoring.patchDownload
From 894520865c4fa697ba6104beb79d64a630f03b3a Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v4 1/3] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

#8Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#1)
Re: amcheck support for BRIN indexes

Hi,

I would like share some thoughts about 'heap all consistent' part and
one of the open questions:

The idea behind 'heap all consistent' is to use heap data to validate
the index. BRIN doesn't store heap tuples, so there is no
straightforward way to check if every tuple was indexed or not. We
have range data, so we need to do something with every heap tuple and
corresponding range. Something that will tell us if the range data
covers the heap tuple or not. What options I see here:

1) We can use the addValue() function. It returns FALSE if range data
was not changed (in other words range data already covers heap tuple
data that we pass to the function). It's very easy to do, we can use
heap tuples directly. But the problem I see here is that addValue()
can return TRUE even if heap tuple data have been already covered by
the range, but range data itself changed for some reason (some new
algorithm were applied for instance). So I think we can have some
false positives that we can do nothing about.

2) We can use the consistent() function. It requires ScanKey and
returns true if the range data satisfies ScanKey's condition. So we
need to convert every heap tuple into ScanKey somehow. This approach
is implemented now in the patch, so I tried to describe all details
about heap tuple to ScanKey conversion in the comment:

/*
* Prepare ScanKey for index attribute.
*
* ConsistentFn requires ScanKey, so we need to generate ScanKey for every
* attribute somehow. We want ScanKey that would result in TRUE for every heap
* tuple within the range when we use its indexed value as sk_argument.
* To generate such a ScanKey we need to define the right operand type
and the strategy number.
* Right operand type is a type of data that index is built on, so
it's 'opcintype'.
* There is no strategy number that we can always use,
* because every opclass defines its own set of operators it supports
and strategy number
* for the same operator can differ from opclass to opclass.
* So to get strategy number we look up an operator that gives us
desired behavior
* and which both operand types are 'opcintype' and then retrieve the
strategy number for it.
* Most of the time we can use '='. We let user define operator name
in case opclass doesn't
* support '=' operator. Also, if such operator doesn't exist, we
can't proceed with the check.
*
* Generated once, and will be reused for all heap tuples.
* Argument field will be filled for every heap tuple before
* consistent function invocation, so leave it NULL for a while.
*
*/

With this approach function brin_check() has optional parameter
'consistent_operator_names' that it seems to me could be very
confusing for users. In general I think this is the most complex
approach both in terms of use and implementation.

3) The approach that seems to me the most clear and straightforward:
to add new optional function to BRIN opclass API. The function that
would say if passed value is covered with the current range data. it's
exactly what we want to know, so we can use heap data directly here.
Something like that:

bool withinRange(BrinDesc *bdesc, BrinValues *column, Datum val, bool isnull)

It could be an optional function that will be implemented for all core
BRIN opclasses. So if somebody wants to use it for some custom opclass
they will need to implement it too, but it's not required. I
understand that adding something to the index opclass API requires
very strong arguments. So the argument here is that it will let to do
brin check very robust (without possible false positives as in the
first approach) and easy to use (no additional parameters in the check
function). Also, the withinRange() function could be written in such a
way that it would be more efficient for our task than addValue() or
consistent().

I'd be glad to hear your thoughts!

Best regards,
Arseniy Mukhin

#9Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#8)
5 attachment(s)
Re: amcheck support for BRIN indexes

Hi!

On Wed, Jun 18, 2025 at 11:33 AM Arseniy Mukhin
<arseniy.mukhin.dev@gmail.com> wrote:

...
On Mon, Jun 16, 2025 at 8:11 PM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
...

If it's not very difficult - it would be great to use read_stream infrastructure. See btvacuumscan() in nbtree.c calling read_stream_begin_relation() for example. We cannot use it in logical scans in B-tree\GiST\GIN, but maybe BRIN can take some advantage of this new shiny technology.

Thanks, I will look into it.

You are right, it was not very difficult to replace index sequential
scans with read_streams. Hope I picked correct stream_flag values.
Thank you!

On Sun, Jun 22, 2025 at 12:55 AM Arseniy Mukhin
<arseniy.mukhin.dev@gmail.com> wrote:

...
1) We can use the addValue() function. It returns FALSE if range data
was not changed (in other words range data already covers heap tuple
data that we pass to the function). It's very easy to do, we can use
heap tuples directly. But the problem I see here is that addValue()
can return TRUE even if heap tuple data have been already covered by
the range, but range data itself changed for some reason (some new
algorithm were applied for instance). So I think we can have some
false positives that we can do nothing about.

And yes, it's not an option really. It turned out that minmax_multi
can return true from addValue() even if it already contains the value.
So we can drop this option.

...
3) The approach that seems to me the most clear and straightforward:
to add new optional function to BRIN opclass API. The function that
would say if passed value is covered with the current range data. it's
exactly what we want to know, so we can use heap data directly here.
Something like that:

bool withinRange(BrinDesc *bdesc, BrinValues *column, Datum val, bool isnull)

It could be an optional function that will be implemented for all core
BRIN opclasses. So if somebody wants to use it for some custom opclass
they will need to implement it too, but it's not required. I
understand that adding something to the index opclass API requires
very strong arguments. So the argument here is that it will let to do
brin check very robust (without possible false positives as in the
first approach) and easy to use (no additional parameters in the check
function). Also, the withinRange() function could be written in such a
way that it would be more efficient for our task than addValue() or
consistent().

I decided to give it a try and implement such a support function. It
was not very difficult since all necessary logic already exists in
addValue() and consistent() functions for all core operator classes.
The main doubt about this approach: we add something to the core just
to use it in the contrib module. But the logic of this method is very
common with what we already have there and probably it is not possible
to implement it outside of the core, because you need all opclass
internals etc.

So there is a new version. I renamed 'heap all consistent' -> 'heap
all indexed', as btree amcheck calls it. I think there is not much
point in using another name here. There are two new files:
0004 - adds new BRIN support function (withinRange).
0005 - migrate 'heap all indexed' from using consistent function to
new withinRange function.

Patch 0003 still has the old 'heap all indexed' implementation that
uses a consistent function (2nd approach). So If you want to have
'heap all indexed' using a consistent function - don't apply 0004 and
0005 patches.

Best regards,
Arseniy Mukhin

Attachments:

v5-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v5-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From 068b7d160c3dff49653f8d1db7bdf0deccfd7b6f Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v5 2/5] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 102 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 ++++++++
 contrib/amcheck/verify_brin.c           | 855 ++++++++++++++++++++++++
 8 files changed, 1408 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..9ec046bb1cf
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..bebca93d32f
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..0a5e26ea8f5
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..04e65314796
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,855 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v5-0005-using-withinRange-function-for-heap-all-indexed-c.patchtext/x-patch; charset=US-ASCII; name=v5-0005-using-withinRange-function-for-heap-all-indexed-c.patchDownload
From 66c5ff50999073a6691cd6a6e4456146c2bd8f29 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sat, 5 Jul 2025 23:12:28 +0300
Subject: [PATCH v5 5/5] using withinRange function for heap all indexed check

---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   5 +-
 contrib/amcheck/expected/check_brin.out |   4 +-
 contrib/amcheck/sql/check_brin.sql      |   4 +-
 contrib/amcheck/verify_brin.c           | 292 +++++-------------------
 4 files changed, 57 insertions(+), 248 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 6337e065bb1..d4f44495bba 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -9,12 +9,11 @@
 --
 CREATE FUNCTION brin_index_check(index regclass,
                                  regularpagescheck boolean default false,
-                                 heapallindexed boolean default false,
-                                 consistent_operator_names text[] default '{}'
+                                 heapallindexed boolean default false
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index 0aa90dafa20..05067858aa9 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0f58567f76f..7993ee0f4d9 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 01a69b616cc..a60499978b3 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -40,7 +40,6 @@ typedef struct BrinCheckState
 
 	bool		regularpagescheck;
 	bool		heapallindexed;
-	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -71,14 +70,9 @@ typedef struct BrinCheckState
 
 	/* Heap all indexed check fields */
 
-	String	  **operatorNames;
 	BrinRevmap *revmap;
 	Buffer		buf;
-	FmgrInfo   *consistentFn;
-	/* Scan keys for regular values */
-	ScanKey    *nonnull_sk;
-	/* Scan keys for null values */
-	ScanKey    *isnull_sk;
+	FmgrInfo   *withinRangeFn;
 	double		range_cnt;
 	/* first block of the next range */
 	BlockNumber nextrangeBlk;
@@ -115,8 +109,6 @@ static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
 static void check_heap_all_indexed(BrinCheckState * state);
 
-static void check_and_prepare_operator_names(BrinCheckState * state);
-
 static void brin_check_callback(Relation index,
 								ItemPointer tid,
 								Datum *values,
@@ -126,10 +118,6 @@ static void brin_check_callback(Relation index,
 
 static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
 
-static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
-
-static ScanKey prepare_isnull_scan_key(AttrNumber attno);
-
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -148,7 +136,6 @@ brin_index_check(PG_FUNCTION_ARGS)
 
 	state->regularpagescheck = PG_GETARG_BOOL(1);
 	state->heapallindexed = PG_GETARG_BOOL(2);
-	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -173,15 +160,6 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
-	/*
-	 * We know how many attributes index has, so let's process operator names
-	 * array
-	 */
-	if (state->heapallindexed)
-	{
-		check_and_prepare_operator_names(state);
-	}
-
 	check_brin_index_structure(state);
 
 	if (state->heapallindexed)
@@ -848,8 +826,8 @@ PageGetItemIdCareful(BrinCheckState * state)
 /*
  * Check that every heap tuple are consistent with the index.
  *
- * Here we generate ScanKey for every heap tuple and test it against
- * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ * We use withinRange function for every nonnull value (in case
+ * of io_regular_nulls = false we use withinRange function for null values too).
  *
  * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
  * are not too "narrow" for each range, which means:
@@ -875,12 +853,10 @@ check_heap_all_indexed(BrinCheckState * state)
 	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
 	state->dtup = brin_new_memtuple(state->bdesc);
 	state->checkable_range = false;
-	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->withinRangeFn = palloc0_array(FmgrInfo, state->natts);
 	state->range_cnt = 0;
 	/* next range is the first range in the beginning */
 	state->nextrangeBlk = 0;
-	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
-	state->isnull_sk = palloc0_array(ScanKey, state->natts);
 	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
 											"brin check range context",
 											ALLOCSET_DEFAULT_SIZES);
@@ -888,19 +864,32 @@ check_heap_all_indexed(BrinCheckState * state)
 												"brin check tuple context",
 												ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
-	 * attribute
-	 */
+	/* Prepare withinRange function for each attribute */
 	for (AttrNumber attno = 1; attno <= state->natts; attno++)
 	{
-		FmgrInfo   *tmp;
+		if (RegProcedureIsValid(index_getprocid(state->idxrel, attno, BRIN_PROCNUM_WITHINRANGE)))
+		{
+			FmgrInfo   *fn = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_WITHINRANGE);
 
-		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
-		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+			fmgr_info_copy(&state->withinRangeFn[attno - 1], fn, CurrentMemoryContext);
+		}
+		else
+		{
+			/*
+			 * If there is no withinRange function for the attribute, notice
+			 * user about it. We continue heap all indexed check even for
+			 * indexes with just one attribute as even without support
+			 * function some errors could be detected.
+			 */
+			ereport(NOTICE,
+					errmsg("Missing support function %d for attribute %d of index \"%s\". "
+						   "Skip heap all indexed check for this attribute.",
+						   BRIN_PROCNUM_WITHINRANGE,
+						   attno,
+						   RelationGetRelationName(state->idxrel)
+						   ));
+		}
 
-		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
-		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
 	}
 
 	indexInfo = BuildIndexInfo(idxrel);
@@ -926,152 +915,6 @@ check_heap_all_indexed(BrinCheckState * state)
 	MemoryContextDelete(state->heaptupleCtx);
 }
 
-/*
- * Check operator names array input parameter and convert it to array of strings
- * Empty input array means we use "=" operator for every attribute
- */
-static void
-check_and_prepare_operator_names(BrinCheckState * state)
-{
-	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
-	int16		typlen;
-	bool		typbyval;
-	char		typalign;
-	Datum	   *values;
-	bool	   *elem_nulls;
-	int			num_elems;
-
-	state->operatorNames = palloc(sizeof(String) * state->natts);
-
-	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
-	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
-					  &values, &elem_nulls, &num_elems);
-
-	/* If we have some input check it and convert to String** */
-	if (num_elems != 0)
-	{
-		if (num_elems != state->natts)
-		{
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("Operator names array length %u, but index has %u attributes",
-							num_elems, state->natts)));
-		}
-
-		for (int i = 0; i < num_elems; i++)
-		{
-			if (elem_nulls[i])
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("Operator names array contains NULL")));
-			}
-			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
-		}
-	}
-	else
-	{
-		/* If there is no input just use "=" operator for all attributes */
-		for (int i = 0; i < state->natts; i++)
-		{
-			state->operatorNames[i] = makeString("=");
-		}
-	}
-}
-
-/*
- * Prepare ScanKey for index attribute.
- *
- * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
- * attribute somehow. We want ScanKey that would result in TRUE for every heap
- * tuple within the range when we use its indexed value as sk_argument.
- * To generate such a ScanKey we need to define the right operand type and the strategy number.
- * Right operand type is a type of data that index is built on, so it's 'opcintype'.
- * There is no strategy number that we can always use,
- * because every opclass defines its own set of operators it supports and strategy number
- * for the same operator can differ from opclass to opclass.
- * So to get strategy number we look up an operator that gives us desired behavior
- * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
- * Most of the time we can use '='. We let user define operator name in case opclass doesn't
- * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
- *
- * Generated once, and will be reused for all heap tuples.
- * Argument field will be filled for every heap tuple before
- * consistent function invocation, so leave it NULL for a while.
- *
- */
-static ScanKey
-prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
-{
-	ScanKey		scanKey;
-	Oid			opOid;
-	Oid			opFamilyOid;
-	bool		defined;
-	StrategyNumber strategy;
-	RegProcedure opRegProc;
-	List	   *operNameList;
-	int			attindex = attno - 1;
-	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
-	Oid			type = state->idxrel->rd_opcintype[attindex];
-	String	   *opname = state->operatorNames[attno - 1];
-
-	opFamilyOid = state->idxrel->rd_opfamily[attindex];
-	operNameList = list_make1(opname);
-	opOid = OperatorLookup(operNameList, type, type, &defined);
-
-	if (opOid == InvalidOid)
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_FUNCTION),
-				 errmsg("There is no operator %s for type %u",
-						opname->sval, type)));
-	}
-
-	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
-
-	if (strategy == 0)
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("operator %s is not a member of operator family \"%s\"",
-						opname->sval,
-						get_opfamily_name(opFamilyOid, false))));
-	}
-
-	opRegProc = get_opcode(opOid);
-	scanKey = palloc0(sizeof(ScanKeyData));
-	ScanKeyEntryInitialize(
-						   scanKey,
-						   0,
-						   attno,
-						   strategy,
-						   type,
-						   attr->attcollation,
-						   opRegProc,
-						   (Datum) NULL
-		);
-	pfree(operNameList);
-
-	return scanKey;
-}
-
-static ScanKey
-prepare_isnull_scan_key(AttrNumber attno)
-{
-	ScanKey		scanKey;
-
-	scanKey = palloc0(sizeof(ScanKeyData));
-	ScanKeyEntryInitialize(scanKey,
-						   SK_ISNULL | SK_SEARCHNULL,
-						   attno,
-						   InvalidStrategy,
-						   InvalidOid,
-						   InvalidOid,
-						   InvalidOid,
-						   (Datum) 0);
-	return scanKey;
-}
-
 /*
  * We walk from the first range (blkno = 0) to the last as the scan proceed.
  * For every heap tuple we check if we are done with the current range, and we need to move further
@@ -1155,10 +998,8 @@ brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull
 
 /*
  * We check hasnulls flags for null values and oi_regular_nulls = true,
- * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
- * For all other cases we call consistentFn with appropriate scanKey:
- * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
- * - for nonnull values we use 'nonnull' scanKey
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is true or not,
+ * We use withinRangeFn for all nonnull values and null values if io_regular_nulls = false.
  */
 static void
 check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
@@ -1177,78 +1018,47 @@ check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls,
 	for (attindex = 0; attindex < state->natts; attindex++)
 	{
 		BrinValues *bval;
-		Datum		consistentFnResult;
-		bool		consistent;
-		ScanKey		scanKey;
+		Datum		withinRangeFnResult;
+		bool		withinRange;
 		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
 
 		bval = &dtup->bt_columns[attindex];
 
-		if (nulls[attindex])
+		/*
+		 * Use hasnulls flag for oi_regular_nulls is true. Otherwise, delegate
+		 * check to withinRangeFn
+		 */
+		if (nulls[attindex] && oi_regular_nulls)
 		{
-			/*
-			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
-			 * delegate check to consistentFn
-			 */
-			if (oi_regular_nulls)
+			/* We have null value, so hasnulls or allnulls must be true */
+			if (!(bval->bv_hasnulls || bval->bv_allnulls))
 			{
-				/* We have null value, so hasnulls or allnulls must be true */
-				if (!(bval->bv_hasnulls || bval->bv_allnulls))
-				{
-					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
-				}
-				continue;
+				all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
 			}
-
-			/*
-			 * In case of null and oi_regular_nulls = false we use isNull
-			 * scanKey for invocation of consistentFn
-			 */
-			scanKey = state->isnull_sk[attindex];
+			continue;
 		}
-		else
-		{
-			/* We have a nonnull value, so allnulls should be false */
-			if (bval->bv_allnulls)
-			{
-				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
-			}
 
-			/* use "attr = value" scan key for nonnull values */
-			scanKey = state->nonnull_sk[attindex];
-			scanKey->sk_argument = values[attindex];
+		/* If we have a nonnull value allnulls should be false */
+		if (!nulls[attindex] && bval->bv_allnulls)
+		{
+			all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
 		}
 
 		/* If oi_regular_nulls = true we should never get there with null */
 		Assert(!oi_regular_nulls || !nulls[attindex]);
 
-		if (state->consistentFn[attindex].fn_nargs >= 4)
-		{
-			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
-												   state->idxrel->rd_indcollation[attindex],
-												   PointerGetDatum(state->bdesc),
-												   PointerGetDatum(bval),
-												   PointerGetDatum(&scanKey),
-												   Int32GetDatum(1)
-				);
-		}
-		else
-		{
-			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
-												   state->idxrel->rd_indcollation[attindex],
-												   PointerGetDatum(state->bdesc),
-												   PointerGetDatum(bval),
-												   PointerGetDatum(scanKey)
-				);
-		}
-
-		consistent = DatumGetBool(consistentFnResult);
+		withinRangeFnResult = FunctionCall4Coll(&state->withinRangeFn[attindex],
+												state->idxrel->rd_indcollation[attindex],
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												values[attindex],
+												nulls[attindex]);
 
-		if (!consistent)
+		withinRange = DatumGetBool(withinRangeFnResult);
+		if (!withinRange)
 		{
 			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
 		}
-
 	}
 
 	MemoryContextSwitchTo(oldCtx);
-- 
2.43.0

v5-0003-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v5-0003-amcheck-brin_index_check-heap-all-indexed.patchDownload
From 9e4bfbd0c430ce0998d0b731bf2756af91f80655 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:41:34 +0300
Subject: [PATCH v5 3/5] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
heap_all_consistent check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  18 +-
 contrib/amcheck/sql/check_brin.sql      |  18 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 501 +++++++++++++++++++++++-
 5 files changed, 563 insertions(+), 31 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 9ec046bb1cf..6337e065bb1 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regular_pages_check boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index bebca93d32f..0aa90dafa20 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0a5e26ea8f5..0f58567f76f 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -35,12 +35,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -52,12 +52,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -69,12 +69,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 04e65314796..01a69b616cc 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -38,7 +38,9 @@ typedef struct BrinCheckState
 
 	/* Check arguments */
 
-	bool		regular_pages_check;
+	bool		regularpagescheck;
+	bool		heapallindexed;
+	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -102,7 +146,9 @@ brin_index_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
-	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
+	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,21 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heapallindexed)
+	{
+		check_and_prepare_operator_names(state);
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -160,8 +218,13 @@ check_brin_index_structure(BrinCheckState * state)
 	/* Check revmap first, blocks: [1, lastRevmapPage] */
 	check_revmap(state);
 
-	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
-	check_regular_pages(state);
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
 }
 
 /* Meta page check and save some data for the further check */
@@ -614,11 +677,6 @@ check_regular_pages(BrinCheckState * state)
 	ReadStreamBlockNumberCB stream_cb;
 	BlockRangeReadStreamPrivate stream_data;
 
-	if (!state->regular_pages_check)
-	{
-		return;
-	}
-
 	/* reset state */
 	state->revmapBlk = InvalidBlockNumber;
 	state->revmapbuf = InvalidBuffer;
@@ -628,7 +686,6 @@ check_regular_pages(BrinCheckState * state)
 	state->regpageoffset = InvalidOffsetNumber;
 	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
 
-
 	/*
 	 * Prepare stream data for regular pages walk. It is safe to use batchmode
 	 * as block_range_read_stream_cb takes no locks.
@@ -788,6 +845,414 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -853,3 +1318,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

v5-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v5-0001-brin-refactoring.patchDownload
From c3fc6e3665f647785cd137a6f608e6e979cb537c Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v5 1/5] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v5-0004-Adds-new-BRIN-support-function-withinRange.patchtext/x-patch; charset=US-ASCII; name=v5-0004-Adds-new-BRIN-support-function-withinRange.patchDownload
From b261e03f709de1016783460c4628151365d52c89 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sat, 5 Jul 2025 23:10:55 +0300
Subject: [PATCH v5 4/5] Adds new BRIN support function 'withinRange'

There is no straightforward way to say if some indexed value is covered
by the range value or not. The new support function provides such a
functionality. Commit adds implementations for all core BRIN
opclasses: minmax, minmax_multi, bloom, inclusion.
---
 src/backend/access/brin/brin_bloom.c        |  44 ++++
 src/backend/access/brin/brin_inclusion.c    |  68 ++++++
 src/backend/access/brin/brin_minmax.c       |  57 ++++++
 src/backend/access/brin/brin_minmax_multi.c | 136 ++++++++----
 src/include/access/brin_internal.h          |  13 +-
 src/include/catalog/pg_amproc.dat           | 216 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat             |  16 ++
 7 files changed, 509 insertions(+), 41 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 82b425ce37d..4fa5e39f0ac 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -584,6 +584,50 @@ brin_bloom_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_bloom_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		contains;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then check if bloom filter contains the value.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, val));
+
+	contains = bloom_contains_value(filter, hashValue);
+
+	PG_RETURN_BOOL(contains);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's bloom
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index b86ca5744a3..0b69da3de91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -237,6 +237,74 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(true);
 }
 
+/*
+ * If the passed value is outside the inclusion range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_inclusion_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *finfo;
+	bool		within_range;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Consistent function returns TRUE for any value if the range contains
+	 * unmergeable values. We follow the same logic here.
+	 */
+	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If the opclass supports the concept of empty values, test the passed
+	 * value for emptiness
+	 */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_EMPTY, true);
+	if (finfo != NULL && DatumGetBool(FunctionCall1Coll(finfo, colloid, newval)))
+	{
+		/* Value is empty but the range doesn't contain empty element */
+		if (!DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]))
+		{
+			PG_RETURN_BOOL(false);
+		}
+
+		/* Value is empty and the range contains empty element */
+		PG_RETURN_BOOL(true);
+	}
+
+	/* Use contains function to check if the range contains the value */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_CONTAINS, true);
+
+	/* Contains function is optional, but this implementation needs it */
+	if (finfo == NULL)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("The operator class is missing support function %d for column %d.",
+						PROCNUM_CONTAINS, attno)));
+	}
+
+	within_range = DatumGetBool(FunctionCall2Coll(finfo, colloid,
+												  column->bv_values[INCLUSION_UNION],
+												  newval));
+	PG_RETURN_BOOL(within_range);
+}
+
 /*
  * BRIN inclusion consistent function
  *
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index d21ab3a668c..37a5dd103de 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -124,6 +124,63 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+/*
+ * If the passed value is outside the min/max range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *cmpFn;
+	Datum		compar;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is less than the range minimum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTLessStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[0]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is greater than the range maximum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTGreaterStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[1]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * The value is greater than / equals the minimum and is less than /
+	 * equals the maximum so it's within the range
+	 */
+	PG_RETURN_BOOL(true);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 0d1507a2a36..2141c47bc71 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -270,6 +270,9 @@ typedef struct compare_context
 static int	compare_values(const void *a, const void *b, void *arg);
 
 
+static Ranges *deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+									   AttrNumber attno);
+
 #ifdef USE_ASSERT_CHECKING
 /*
  * Check that the order of the array values is correct, using the cmp
@@ -2421,7 +2424,6 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 	Ranges	   *ranges;
-	SerializedRanges *serialized = NULL;
 
 	Assert(!isnull);
 
@@ -2489,55 +2491,119 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	}
 	else if (!ranges)
 	{
-		MemoryContext oldctx;
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
+	}
 
-		int			maxvalues;
-		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	PG_RETURN_BOOL(modified);
+}
 
-		/*
-		 * Determine the insert buffer size - we use 10x the target, capped to
-		 * the maximum number of values in the heap range. This is more than
-		 * enough, considering the actual number of rows per page is likely
-		 * much lower, but meh.
-		 */
-		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
-						MaxHeapTuplesPerPage * pagesPerRange);
 
-		/* but always at least the original value */
-		maxvalues = Max(maxvalues, serialized->maxvalues);
+/*
+ * Deserialize range value and save it in bdesc->bv_mem_value for future use
+ */
+Ranges *
+deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+						AttrNumber attno)
+{
+	MemoryContext oldctx;
+	SerializedRanges *serialized = NULL;
+	Ranges	   *ranges;
 
-		/* always cap by MIN/MAX */
-		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
-		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+	int			maxvalues;
+	BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = brin_range_deserialize(maxvalues, serialized);
+	oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		ranges->attno = attno;
-		ranges->colloid = colloid;
-		ranges->typid = attr->atttypid;
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
 
-		/* we'll certainly need the comparator, so just look it up now */
-		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-														 BTLessStrategyNumber);
+	/*
+	 * Determine the insert buffer size - we use 10x the target, capped to the
+	 * maximum number of values in the heap range. This is more than enough,
+	 * considering the actual number of rows per page is likely much lower,
+	 * but meh.
+	 */
+	maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+					MaxHeapTuplesPerPage * pagesPerRange);
 
-		column->bv_mem_value = PointerGetDatum(ranges);
-		column->bv_serialize = brin_minmax_multi_serialize;
+	/* but always at least the original value */
+	maxvalues = Max(maxvalues, serialized->maxvalues);
 
-		MemoryContextSwitchTo(oldctx);
+	/* always cap by MIN/MAX */
+	maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+	maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+	ranges = brin_range_deserialize(maxvalues, serialized);
+
+	ranges->attno = attno;
+	ranges->colloid = colloid;
+	ranges->typid = attr->atttypid;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+													 BTLessStrategyNumber);
+
+	column->bv_mem_value = PointerGetDatum(ranges);
+	column->bv_serialize = brin_minmax_multi_serialize;
+
+	MemoryContextSwitchTo(oldctx);
+
+	return ranges;
+}
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_multi_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		contains = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	FmgrInfo   *cmpFn;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+	else if (!ranges)
+	{
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
 	}
 
-	/*
-	 * Try to add the new value to the range. We need to update the modified
-	 * flag, so that we serialize the updated summary later.
-	 */
-	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
 
-	PG_RETURN_BOOL(modified);
+	/* Use 'full = true' here, as we don't want any false negatives */
+	contains = range_contains_value(bdesc, colloid, attno, attr, ranges, val, true);
+
+	PG_RETURN_BOOL(contains);
 }
 
 /*
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index d093a0bf130..5df87761cf1 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -67,12 +67,13 @@ typedef struct BrinDesc
  * opclasses can define more function support numbers, which must fall into
  * BRIN_FIRST_OPTIONAL_PROCNUM .. BRIN_LAST_OPTIONAL_PROCNUM.
  */
-#define BRIN_PROCNUM_OPCINFO		1
-#define BRIN_PROCNUM_ADDVALUE		2
-#define BRIN_PROCNUM_CONSISTENT		3
-#define BRIN_PROCNUM_UNION			4
-#define BRIN_MANDATORY_NPROCS		4
-#define BRIN_PROCNUM_OPTIONS 		5	/* optional */
+#define BRIN_PROCNUM_OPCINFO		    1
+#define BRIN_PROCNUM_ADDVALUE		    2
+#define BRIN_PROCNUM_CONSISTENT		    3
+#define BRIN_PROCNUM_UNION			    4
+#define BRIN_MANDATORY_NPROCS		    4
+#define BRIN_PROCNUM_OPTIONS 		    5	/* optional */
+#define BRIN_PROCNUM_WITHINRANGE 		6	/* optional */
 /* procedure numbers up to 10 are reserved for BRIN future expansion */
 #define BRIN_FIRST_OPTIONAL_PROCNUM 11
 #define BRIN_LAST_OPTIONAL_PROCNUM	15
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa..c3947bbc410 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -847,6 +847,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
@@ -863,6 +866,9 @@
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashbytea' },
 
@@ -878,6 +884,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
@@ -892,6 +901,9 @@
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
 
@@ -907,6 +919,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
@@ -921,6 +936,9 @@
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
 
@@ -936,6 +954,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '1',
@@ -948,6 +969,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '1',
@@ -960,6 +984,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
@@ -977,6 +1004,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int2' },
@@ -996,6 +1026,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1015,6 +1048,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int8' },
@@ -1032,6 +1068,9 @@
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
 
@@ -1047,6 +1086,9 @@
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
 
@@ -1062,6 +1104,9 @@
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1077,6 +1122,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
@@ -1091,6 +1139,9 @@
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
 
@@ -1105,6 +1156,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi oid
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
@@ -1122,6 +1176,9 @@
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1139,6 +1196,9 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
 
@@ -1153,6 +1213,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom tid
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
@@ -1167,6 +1230,9 @@
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
@@ -1186,6 +1252,9 @@
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_tid' },
@@ -1203,6 +1272,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '1',
@@ -1216,6 +1288,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi float
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
@@ -1233,6 +1308,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float4' },
@@ -1252,6 +1330,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float8' },
@@ -1271,6 +1352,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11', amproc => 'hashfloat4' },
 
@@ -1288,6 +1372,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11', amproc => 'hashfloat8' },
 
@@ -1304,6 +1391,9 @@
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
@@ -1321,6 +1411,9 @@
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr' },
@@ -1341,6 +1434,9 @@
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11', amproc => 'hashmacaddr' },
 
@@ -1357,6 +1453,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
@@ -1374,6 +1473,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_macaddr8' },
@@ -1394,6 +1496,9 @@
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '11', amproc => 'hashmacaddr8' },
 
@@ -1409,6 +1514,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi inet
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
@@ -1426,6 +1534,9 @@
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_inet' },
@@ -1443,6 +1554,9 @@
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
 
@@ -1459,6 +1573,9 @@
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'inet_merge' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
@@ -1479,6 +1596,9 @@
 { amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
@@ -1495,6 +1615,9 @@
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '11', amproc => 'hashbpchar' },
 
@@ -1510,6 +1633,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time without time zone
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
@@ -1527,6 +1653,9 @@
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_time' },
@@ -1544,6 +1673,9 @@
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
 
@@ -1560,6 +1692,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '1',
@@ -1573,6 +1708,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1',
@@ -1585,6 +1723,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
@@ -1602,6 +1743,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1621,6 +1765,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1640,6 +1787,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_date' },
@@ -1660,6 +1810,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1679,6 +1832,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1695,6 +1851,9 @@
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1711,6 +1870,9 @@
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi interval
 { amprocfamily => 'brin/interval_minmax_multi_ops',
@@ -1728,6 +1890,9 @@
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_interval' },
@@ -1748,6 +1913,9 @@
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '11', amproc => 'interval_hash' },
 
@@ -1764,6 +1932,9 @@
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time with time zone
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
@@ -1781,6 +1952,9 @@
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_timetz' },
@@ -1800,6 +1974,9 @@
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11', amproc => 'timetz_hash' },
 
@@ -1814,6 +1991,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
@@ -1828,6 +2008,9 @@
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
@@ -1842,6 +2025,9 @@
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi numeric
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
@@ -1859,6 +2045,9 @@
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_numeric' },
@@ -1879,6 +2068,9 @@
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11', amproc => 'hash_numeric' },
 
@@ -1894,6 +2086,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi uuid
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
@@ -1911,6 +2106,9 @@
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_uuid' },
@@ -1928,6 +2126,9 @@
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
 
@@ -1944,6 +2145,9 @@
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '11',
   amproc => 'range_merge(anyrange,anyrange)' },
@@ -1967,6 +2171,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi pg_lsn
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
@@ -1984,6 +2191,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_pg_lsn' },
@@ -2003,6 +2213,9 @@
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11', amproc => 'pg_lsn_hash' },
 
@@ -2019,6 +2232,9 @@
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '11', amproc => 'bound_box' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d4650947c63..fb1fa1581b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8904,6 +8904,10 @@
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
+{ oid => '9637', descr => 'BRIN minmax support',
+  proname => 'brin_minmax_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_within_range' },
 
 # BRIN minmax multi
 { oid => '4616', descr => 'BRIN multi minmax support',
@@ -8925,6 +8929,10 @@
   proname => 'brin_minmax_multi_options', proisstrict => 'f',
   prorettype => 'void', proargtypes => 'internal',
   prosrc => 'brin_minmax_multi_options' },
+{ oid => '9638', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_within_range' },
 
 { oid => '4621', descr => 'BRIN multi minmax int2 distance',
   proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
@@ -9011,6 +9019,10 @@
   proname => 'brin_inclusion_union', prorettype => 'bool',
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
+{ oid => '9639', descr => 'BRIN inclusion support',
+  proname => 'brin_inclusion_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_inclusion_within_range' },
 
 # BRIN bloom
 { oid => '4591', descr => 'BRIN bloom support',
@@ -9030,6 +9042,10 @@
 { oid => '4595', descr => 'BRIN bloom support',
   proname => 'brin_bloom_options', proisstrict => 'f', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+{ oid => '9640', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_within_range' },
 
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
-- 
2.43.0

#10Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#9)
5 attachment(s)
Re: amcheck support for BRIN indexes

Sorry, forget to run a full test run with the new patch version. Some
tests were unhappy with the new unknown support function. Here the new
version with the fix.

Best regards,
Arseniy Mukhin

Attachments:

v6-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v6-0001-brin-refactoring.patchDownload
From c3fc6e3665f647785cd137a6f608e6e979cb537c Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v6 1/5] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 2 +-
 src/backend/access/common/reloptions.c | 3 ++-
 src/include/access/brin.h              | 1 +
 src/include/access/brin_tuple.h        | 2 ++
 4 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..4d1d8d9addd 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,7 +57,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
+TupleDesc
 brtuple_disk_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..9472ca638dd 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brtuple_disk_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v6-0003-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v6-0003-amcheck-brin_index_check-heap-all-indexed.patchDownload
From 9e4bfbd0c430ce0998d0b731bf2756af91f80655 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:41:34 +0300
Subject: [PATCH v6 3/5] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
heap_all_consistent check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  18 +-
 contrib/amcheck/sql/check_brin.sql      |  18 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 501 +++++++++++++++++++++++-
 5 files changed, 563 insertions(+), 31 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 9ec046bb1cf..6337e065bb1 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regular_pages_check boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index bebca93d32f..0aa90dafa20 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0a5e26ea8f5..0f58567f76f 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -35,12 +35,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -52,12 +52,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -69,12 +69,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 04e65314796..01a69b616cc 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -38,7 +38,9 @@ typedef struct BrinCheckState
 
 	/* Check arguments */
 
-	bool		regular_pages_check;
+	bool		regularpagescheck;
+	bool		heapallindexed;
+	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -102,7 +146,9 @@ brin_index_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
-	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
+	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,21 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heapallindexed)
+	{
+		check_and_prepare_operator_names(state);
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -160,8 +218,13 @@ check_brin_index_structure(BrinCheckState * state)
 	/* Check revmap first, blocks: [1, lastRevmapPage] */
 	check_revmap(state);
 
-	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
-	check_regular_pages(state);
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
 }
 
 /* Meta page check and save some data for the further check */
@@ -614,11 +677,6 @@ check_regular_pages(BrinCheckState * state)
 	ReadStreamBlockNumberCB stream_cb;
 	BlockRangeReadStreamPrivate stream_data;
 
-	if (!state->regular_pages_check)
-	{
-		return;
-	}
-
 	/* reset state */
 	state->revmapBlk = InvalidBlockNumber;
 	state->revmapbuf = InvalidBuffer;
@@ -628,7 +686,6 @@ check_regular_pages(BrinCheckState * state)
 	state->regpageoffset = InvalidOffsetNumber;
 	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
 
-
 	/*
 	 * Prepare stream data for regular pages walk. It is safe to use batchmode
 	 * as block_range_read_stream_cb takes no locks.
@@ -788,6 +845,414 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use "attr = value" scan key for nonnull values */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -853,3 +1318,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

v6-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v6-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From 068b7d160c3dff49653f8d1db7bdf0deccfd7b6f Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v6 2/5] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 102 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 ++++++++
 contrib/amcheck/verify_brin.c           | 855 ++++++++++++++++++++++++
 8 files changed, 1408 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..9ec046bb1cf
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..bebca93d32f
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..0a5e26ea8f5
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,102 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multi attributes with varlena attribute test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,5000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..04e65314796
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,855 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brtuple_disk_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v6-0005-using-withinRange-function-for-heap-all-indexed-c.patchtext/x-patch; charset=US-ASCII; name=v6-0005-using-withinRange-function-for-heap-all-indexed-c.patchDownload
From 34375eafec828bc96ffbbbd6e2eb432ef5424daa Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sun, 6 Jul 2025 21:47:05 +0300
Subject: [PATCH v6 5/5] using withinRange function for heap all indexed check

---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   5 +-
 contrib/amcheck/expected/check_brin.out |   4 +-
 contrib/amcheck/sql/check_brin.sql      |   4 +-
 contrib/amcheck/verify_brin.c           | 292 +++++-------------------
 4 files changed, 57 insertions(+), 248 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 6337e065bb1..d4f44495bba 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -9,12 +9,11 @@
 --
 CREATE FUNCTION brin_index_check(index regclass,
                                  regularpagescheck boolean default false,
-                                 heapallindexed boolean default false,
-                                 consistent_operator_names text[] default '{}'
+                                 heapallindexed boolean default false
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index 0aa90dafa20..05067858aa9 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 0f58567f76f..7993ee0f4d9 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -88,12 +88,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 01a69b616cc..a60499978b3 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -40,7 +40,6 @@ typedef struct BrinCheckState
 
 	bool		regularpagescheck;
 	bool		heapallindexed;
-	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -71,14 +70,9 @@ typedef struct BrinCheckState
 
 	/* Heap all indexed check fields */
 
-	String	  **operatorNames;
 	BrinRevmap *revmap;
 	Buffer		buf;
-	FmgrInfo   *consistentFn;
-	/* Scan keys for regular values */
-	ScanKey    *nonnull_sk;
-	/* Scan keys for null values */
-	ScanKey    *isnull_sk;
+	FmgrInfo   *withinRangeFn;
 	double		range_cnt;
 	/* first block of the next range */
 	BlockNumber nextrangeBlk;
@@ -115,8 +109,6 @@ static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
 static void check_heap_all_indexed(BrinCheckState * state);
 
-static void check_and_prepare_operator_names(BrinCheckState * state);
-
 static void brin_check_callback(Relation index,
 								ItemPointer tid,
 								Datum *values,
@@ -126,10 +118,6 @@ static void brin_check_callback(Relation index,
 
 static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
 
-static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
-
-static ScanKey prepare_isnull_scan_key(AttrNumber attno);
-
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -148,7 +136,6 @@ brin_index_check(PG_FUNCTION_ARGS)
 
 	state->regularpagescheck = PG_GETARG_BOOL(1);
 	state->heapallindexed = PG_GETARG_BOOL(2);
-	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -173,15 +160,6 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
-	/*
-	 * We know how many attributes index has, so let's process operator names
-	 * array
-	 */
-	if (state->heapallindexed)
-	{
-		check_and_prepare_operator_names(state);
-	}
-
 	check_brin_index_structure(state);
 
 	if (state->heapallindexed)
@@ -848,8 +826,8 @@ PageGetItemIdCareful(BrinCheckState * state)
 /*
  * Check that every heap tuple are consistent with the index.
  *
- * Here we generate ScanKey for every heap tuple and test it against
- * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ * We use withinRange function for every nonnull value (in case
+ * of io_regular_nulls = false we use withinRange function for null values too).
  *
  * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
  * are not too "narrow" for each range, which means:
@@ -875,12 +853,10 @@ check_heap_all_indexed(BrinCheckState * state)
 	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
 	state->dtup = brin_new_memtuple(state->bdesc);
 	state->checkable_range = false;
-	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->withinRangeFn = palloc0_array(FmgrInfo, state->natts);
 	state->range_cnt = 0;
 	/* next range is the first range in the beginning */
 	state->nextrangeBlk = 0;
-	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
-	state->isnull_sk = palloc0_array(ScanKey, state->natts);
 	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
 											"brin check range context",
 											ALLOCSET_DEFAULT_SIZES);
@@ -888,19 +864,32 @@ check_heap_all_indexed(BrinCheckState * state)
 												"brin check tuple context",
 												ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
-	 * attribute
-	 */
+	/* Prepare withinRange function for each attribute */
 	for (AttrNumber attno = 1; attno <= state->natts; attno++)
 	{
-		FmgrInfo   *tmp;
+		if (RegProcedureIsValid(index_getprocid(state->idxrel, attno, BRIN_PROCNUM_WITHINRANGE)))
+		{
+			FmgrInfo   *fn = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_WITHINRANGE);
 
-		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
-		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+			fmgr_info_copy(&state->withinRangeFn[attno - 1], fn, CurrentMemoryContext);
+		}
+		else
+		{
+			/*
+			 * If there is no withinRange function for the attribute, notice
+			 * user about it. We continue heap all indexed check even for
+			 * indexes with just one attribute as even without support
+			 * function some errors could be detected.
+			 */
+			ereport(NOTICE,
+					errmsg("Missing support function %d for attribute %d of index \"%s\". "
+						   "Skip heap all indexed check for this attribute.",
+						   BRIN_PROCNUM_WITHINRANGE,
+						   attno,
+						   RelationGetRelationName(state->idxrel)
+						   ));
+		}
 
-		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
-		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
 	}
 
 	indexInfo = BuildIndexInfo(idxrel);
@@ -926,152 +915,6 @@ check_heap_all_indexed(BrinCheckState * state)
 	MemoryContextDelete(state->heaptupleCtx);
 }
 
-/*
- * Check operator names array input parameter and convert it to array of strings
- * Empty input array means we use "=" operator for every attribute
- */
-static void
-check_and_prepare_operator_names(BrinCheckState * state)
-{
-	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
-	int16		typlen;
-	bool		typbyval;
-	char		typalign;
-	Datum	   *values;
-	bool	   *elem_nulls;
-	int			num_elems;
-
-	state->operatorNames = palloc(sizeof(String) * state->natts);
-
-	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
-	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
-					  &values, &elem_nulls, &num_elems);
-
-	/* If we have some input check it and convert to String** */
-	if (num_elems != 0)
-	{
-		if (num_elems != state->natts)
-		{
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("Operator names array length %u, but index has %u attributes",
-							num_elems, state->natts)));
-		}
-
-		for (int i = 0; i < num_elems; i++)
-		{
-			if (elem_nulls[i])
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("Operator names array contains NULL")));
-			}
-			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
-		}
-	}
-	else
-	{
-		/* If there is no input just use "=" operator for all attributes */
-		for (int i = 0; i < state->natts; i++)
-		{
-			state->operatorNames[i] = makeString("=");
-		}
-	}
-}
-
-/*
- * Prepare ScanKey for index attribute.
- *
- * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
- * attribute somehow. We want ScanKey that would result in TRUE for every heap
- * tuple within the range when we use its indexed value as sk_argument.
- * To generate such a ScanKey we need to define the right operand type and the strategy number.
- * Right operand type is a type of data that index is built on, so it's 'opcintype'.
- * There is no strategy number that we can always use,
- * because every opclass defines its own set of operators it supports and strategy number
- * for the same operator can differ from opclass to opclass.
- * So to get strategy number we look up an operator that gives us desired behavior
- * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
- * Most of the time we can use '='. We let user define operator name in case opclass doesn't
- * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
- *
- * Generated once, and will be reused for all heap tuples.
- * Argument field will be filled for every heap tuple before
- * consistent function invocation, so leave it NULL for a while.
- *
- */
-static ScanKey
-prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
-{
-	ScanKey		scanKey;
-	Oid			opOid;
-	Oid			opFamilyOid;
-	bool		defined;
-	StrategyNumber strategy;
-	RegProcedure opRegProc;
-	List	   *operNameList;
-	int			attindex = attno - 1;
-	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
-	Oid			type = state->idxrel->rd_opcintype[attindex];
-	String	   *opname = state->operatorNames[attno - 1];
-
-	opFamilyOid = state->idxrel->rd_opfamily[attindex];
-	operNameList = list_make1(opname);
-	opOid = OperatorLookup(operNameList, type, type, &defined);
-
-	if (opOid == InvalidOid)
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_FUNCTION),
-				 errmsg("There is no operator %s for type %u",
-						opname->sval, type)));
-	}
-
-	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
-
-	if (strategy == 0)
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("operator %s is not a member of operator family \"%s\"",
-						opname->sval,
-						get_opfamily_name(opFamilyOid, false))));
-	}
-
-	opRegProc = get_opcode(opOid);
-	scanKey = palloc0(sizeof(ScanKeyData));
-	ScanKeyEntryInitialize(
-						   scanKey,
-						   0,
-						   attno,
-						   strategy,
-						   type,
-						   attr->attcollation,
-						   opRegProc,
-						   (Datum) NULL
-		);
-	pfree(operNameList);
-
-	return scanKey;
-}
-
-static ScanKey
-prepare_isnull_scan_key(AttrNumber attno)
-{
-	ScanKey		scanKey;
-
-	scanKey = palloc0(sizeof(ScanKeyData));
-	ScanKeyEntryInitialize(scanKey,
-						   SK_ISNULL | SK_SEARCHNULL,
-						   attno,
-						   InvalidStrategy,
-						   InvalidOid,
-						   InvalidOid,
-						   InvalidOid,
-						   (Datum) 0);
-	return scanKey;
-}
-
 /*
  * We walk from the first range (blkno = 0) to the last as the scan proceed.
  * For every heap tuple we check if we are done with the current range, and we need to move further
@@ -1155,10 +998,8 @@ brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull
 
 /*
  * We check hasnulls flags for null values and oi_regular_nulls = true,
- * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
- * For all other cases we call consistentFn with appropriate scanKey:
- * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
- * - for nonnull values we use 'nonnull' scanKey
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is true or not,
+ * We use withinRangeFn for all nonnull values and null values if io_regular_nulls = false.
  */
 static void
 check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
@@ -1177,78 +1018,47 @@ check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls,
 	for (attindex = 0; attindex < state->natts; attindex++)
 	{
 		BrinValues *bval;
-		Datum		consistentFnResult;
-		bool		consistent;
-		ScanKey		scanKey;
+		Datum		withinRangeFnResult;
+		bool		withinRange;
 		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
 
 		bval = &dtup->bt_columns[attindex];
 
-		if (nulls[attindex])
+		/*
+		 * Use hasnulls flag for oi_regular_nulls is true. Otherwise, delegate
+		 * check to withinRangeFn
+		 */
+		if (nulls[attindex] && oi_regular_nulls)
 		{
-			/*
-			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
-			 * delegate check to consistentFn
-			 */
-			if (oi_regular_nulls)
+			/* We have null value, so hasnulls or allnulls must be true */
+			if (!(bval->bv_hasnulls || bval->bv_allnulls))
 			{
-				/* We have null value, so hasnulls or allnulls must be true */
-				if (!(bval->bv_hasnulls || bval->bv_allnulls))
-				{
-					all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
-				}
-				continue;
+				all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
 			}
-
-			/*
-			 * In case of null and oi_regular_nulls = false we use isNull
-			 * scanKey for invocation of consistentFn
-			 */
-			scanKey = state->isnull_sk[attindex];
+			continue;
 		}
-		else
-		{
-			/* We have a nonnull value, so allnulls should be false */
-			if (bval->bv_allnulls)
-			{
-				all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
-			}
 
-			/* use "attr = value" scan key for nonnull values */
-			scanKey = state->nonnull_sk[attindex];
-			scanKey->sk_argument = values[attindex];
+		/* If we have a nonnull value allnulls should be false */
+		if (!nulls[attindex] && bval->bv_allnulls)
+		{
+			all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
 		}
 
 		/* If oi_regular_nulls = true we should never get there with null */
 		Assert(!oi_regular_nulls || !nulls[attindex]);
 
-		if (state->consistentFn[attindex].fn_nargs >= 4)
-		{
-			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
-												   state->idxrel->rd_indcollation[attindex],
-												   PointerGetDatum(state->bdesc),
-												   PointerGetDatum(bval),
-												   PointerGetDatum(&scanKey),
-												   Int32GetDatum(1)
-				);
-		}
-		else
-		{
-			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
-												   state->idxrel->rd_indcollation[attindex],
-												   PointerGetDatum(state->bdesc),
-												   PointerGetDatum(bval),
-												   PointerGetDatum(scanKey)
-				);
-		}
-
-		consistent = DatumGetBool(consistentFnResult);
+		withinRangeFnResult = FunctionCall4Coll(&state->withinRangeFn[attindex],
+												state->idxrel->rd_indcollation[attindex],
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												values[attindex],
+												nulls[attindex]);
 
-		if (!consistent)
+		withinRange = DatumGetBool(withinRangeFnResult);
+		if (!withinRange)
 		{
 			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
 		}
-
 	}
 
 	MemoryContextSwitchTo(oldCtx);
-- 
2.43.0

v6-0004-Adds-new-BRIN-support-function-withinRange.patchtext/x-patch; charset=US-ASCII; name=v6-0004-Adds-new-BRIN-support-function-withinRange.patchDownload
From 5335c32956e294b5775e78e4e15d4746ac53054d Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sat, 5 Jul 2025 23:10:55 +0300
Subject: [PATCH v6 4/5] Adds new BRIN support function 'withinRange'

There is no straightforward way to say if some indexed value is covered
by the range value or not. The new support function provides such a
functionality. Commit adds implementations for all core BRIN
opclasses: minmax, minmax_multi, bloom, inclusion.
---
 src/backend/access/brin/brin_bloom.c        |  44 ++++
 src/backend/access/brin/brin_inclusion.c    |  68 ++++++
 src/backend/access/brin/brin_minmax.c       |  57 ++++++
 src/backend/access/brin/brin_minmax_multi.c | 136 ++++++++----
 src/backend/access/brin/brin_validate.c     |   1 +
 src/include/access/brin_internal.h          |  13 +-
 src/include/catalog/pg_amproc.dat           | 216 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat             |  16 ++
 8 files changed, 510 insertions(+), 41 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 82b425ce37d..4fa5e39f0ac 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -584,6 +584,50 @@ brin_bloom_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_bloom_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		contains;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then check if bloom filter contains the value.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, val));
+
+	contains = bloom_contains_value(filter, hashValue);
+
+	PG_RETURN_BOOL(contains);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's bloom
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index b86ca5744a3..0b69da3de91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -237,6 +237,74 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(true);
 }
 
+/*
+ * If the passed value is outside the inclusion range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_inclusion_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *finfo;
+	bool		within_range;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Consistent function returns TRUE for any value if the range contains
+	 * unmergeable values. We follow the same logic here.
+	 */
+	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If the opclass supports the concept of empty values, test the passed
+	 * value for emptiness
+	 */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_EMPTY, true);
+	if (finfo != NULL && DatumGetBool(FunctionCall1Coll(finfo, colloid, newval)))
+	{
+		/* Value is empty but the range doesn't contain empty element */
+		if (!DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]))
+		{
+			PG_RETURN_BOOL(false);
+		}
+
+		/* Value is empty and the range contains empty element */
+		PG_RETURN_BOOL(true);
+	}
+
+	/* Use contains function to check if the range contains the value */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_CONTAINS, true);
+
+	/* Contains function is optional, but this implementation needs it */
+	if (finfo == NULL)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("The operator class is missing support function %d for column %d.",
+						PROCNUM_CONTAINS, attno)));
+	}
+
+	within_range = DatumGetBool(FunctionCall2Coll(finfo, colloid,
+												  column->bv_values[INCLUSION_UNION],
+												  newval));
+	PG_RETURN_BOOL(within_range);
+}
+
 /*
  * BRIN inclusion consistent function
  *
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index d21ab3a668c..37a5dd103de 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -124,6 +124,63 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+/*
+ * If the passed value is outside the min/max range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *cmpFn;
+	Datum		compar;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is less than the range minimum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTLessStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[0]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is greater than the range maximum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTGreaterStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[1]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * The value is greater than / equals the minimum and is less than /
+	 * equals the maximum so it's within the range
+	 */
+	PG_RETURN_BOOL(true);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 0d1507a2a36..2141c47bc71 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -270,6 +270,9 @@ typedef struct compare_context
 static int	compare_values(const void *a, const void *b, void *arg);
 
 
+static Ranges *deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+									   AttrNumber attno);
+
 #ifdef USE_ASSERT_CHECKING
 /*
  * Check that the order of the array values is correct, using the cmp
@@ -2421,7 +2424,6 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 	Ranges	   *ranges;
-	SerializedRanges *serialized = NULL;
 
 	Assert(!isnull);
 
@@ -2489,55 +2491,119 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	}
 	else if (!ranges)
 	{
-		MemoryContext oldctx;
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
+	}
 
-		int			maxvalues;
-		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	PG_RETURN_BOOL(modified);
+}
 
-		/*
-		 * Determine the insert buffer size - we use 10x the target, capped to
-		 * the maximum number of values in the heap range. This is more than
-		 * enough, considering the actual number of rows per page is likely
-		 * much lower, but meh.
-		 */
-		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
-						MaxHeapTuplesPerPage * pagesPerRange);
 
-		/* but always at least the original value */
-		maxvalues = Max(maxvalues, serialized->maxvalues);
+/*
+ * Deserialize range value and save it in bdesc->bv_mem_value for future use
+ */
+Ranges *
+deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+						AttrNumber attno)
+{
+	MemoryContext oldctx;
+	SerializedRanges *serialized = NULL;
+	Ranges	   *ranges;
 
-		/* always cap by MIN/MAX */
-		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
-		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+	int			maxvalues;
+	BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = brin_range_deserialize(maxvalues, serialized);
+	oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		ranges->attno = attno;
-		ranges->colloid = colloid;
-		ranges->typid = attr->atttypid;
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
 
-		/* we'll certainly need the comparator, so just look it up now */
-		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-														 BTLessStrategyNumber);
+	/*
+	 * Determine the insert buffer size - we use 10x the target, capped to the
+	 * maximum number of values in the heap range. This is more than enough,
+	 * considering the actual number of rows per page is likely much lower,
+	 * but meh.
+	 */
+	maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+					MaxHeapTuplesPerPage * pagesPerRange);
 
-		column->bv_mem_value = PointerGetDatum(ranges);
-		column->bv_serialize = brin_minmax_multi_serialize;
+	/* but always at least the original value */
+	maxvalues = Max(maxvalues, serialized->maxvalues);
 
-		MemoryContextSwitchTo(oldctx);
+	/* always cap by MIN/MAX */
+	maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+	maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+	ranges = brin_range_deserialize(maxvalues, serialized);
+
+	ranges->attno = attno;
+	ranges->colloid = colloid;
+	ranges->typid = attr->atttypid;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+													 BTLessStrategyNumber);
+
+	column->bv_mem_value = PointerGetDatum(ranges);
+	column->bv_serialize = brin_minmax_multi_serialize;
+
+	MemoryContextSwitchTo(oldctx);
+
+	return ranges;
+}
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_multi_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		contains = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	FmgrInfo   *cmpFn;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+	else if (!ranges)
+	{
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
 	}
 
-	/*
-	 * Try to add the new value to the range. We need to update the modified
-	 * flag, so that we serialize the updated summary later.
-	 */
-	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
 
-	PG_RETURN_BOOL(modified);
+	/* Use 'full = true' here, as we don't want any false negatives */
+	contains = range_contains_value(bdesc, colloid, attno, attr, ranges, val, true);
+
+	PG_RETURN_BOOL(contains);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 915b8628b46..2c59d7ecca5 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -84,6 +84,7 @@ brinvalidate(Oid opclassoid)
 											1, 1, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_ADDVALUE:
+			case BRIN_PROCNUM_WITHINRANGE:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
 											4, 4, INTERNALOID, INTERNALOID,
 											INTERNALOID, INTERNALOID);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index d093a0bf130..5df87761cf1 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -67,12 +67,13 @@ typedef struct BrinDesc
  * opclasses can define more function support numbers, which must fall into
  * BRIN_FIRST_OPTIONAL_PROCNUM .. BRIN_LAST_OPTIONAL_PROCNUM.
  */
-#define BRIN_PROCNUM_OPCINFO		1
-#define BRIN_PROCNUM_ADDVALUE		2
-#define BRIN_PROCNUM_CONSISTENT		3
-#define BRIN_PROCNUM_UNION			4
-#define BRIN_MANDATORY_NPROCS		4
-#define BRIN_PROCNUM_OPTIONS 		5	/* optional */
+#define BRIN_PROCNUM_OPCINFO		    1
+#define BRIN_PROCNUM_ADDVALUE		    2
+#define BRIN_PROCNUM_CONSISTENT		    3
+#define BRIN_PROCNUM_UNION			    4
+#define BRIN_MANDATORY_NPROCS		    4
+#define BRIN_PROCNUM_OPTIONS 		    5	/* optional */
+#define BRIN_PROCNUM_WITHINRANGE 		6	/* optional */
 /* procedure numbers up to 10 are reserved for BRIN future expansion */
 #define BRIN_FIRST_OPTIONAL_PROCNUM 11
 #define BRIN_LAST_OPTIONAL_PROCNUM	15
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa..c3947bbc410 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -847,6 +847,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
@@ -863,6 +866,9 @@
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashbytea' },
 
@@ -878,6 +884,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
@@ -892,6 +901,9 @@
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
 
@@ -907,6 +919,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
@@ -921,6 +936,9 @@
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
 
@@ -936,6 +954,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '1',
@@ -948,6 +969,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '1',
@@ -960,6 +984,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
@@ -977,6 +1004,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int2' },
@@ -996,6 +1026,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1015,6 +1048,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int8' },
@@ -1032,6 +1068,9 @@
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
 
@@ -1047,6 +1086,9 @@
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
 
@@ -1062,6 +1104,9 @@
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1077,6 +1122,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
@@ -1091,6 +1139,9 @@
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
 
@@ -1105,6 +1156,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi oid
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
@@ -1122,6 +1176,9 @@
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1139,6 +1196,9 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
 
@@ -1153,6 +1213,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom tid
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
@@ -1167,6 +1230,9 @@
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
@@ -1186,6 +1252,9 @@
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_tid' },
@@ -1203,6 +1272,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '1',
@@ -1216,6 +1288,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi float
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
@@ -1233,6 +1308,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float4' },
@@ -1252,6 +1330,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float8' },
@@ -1271,6 +1352,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11', amproc => 'hashfloat4' },
 
@@ -1288,6 +1372,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11', amproc => 'hashfloat8' },
 
@@ -1304,6 +1391,9 @@
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
@@ -1321,6 +1411,9 @@
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr' },
@@ -1341,6 +1434,9 @@
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11', amproc => 'hashmacaddr' },
 
@@ -1357,6 +1453,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
@@ -1374,6 +1473,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_macaddr8' },
@@ -1394,6 +1496,9 @@
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '11', amproc => 'hashmacaddr8' },
 
@@ -1409,6 +1514,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi inet
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
@@ -1426,6 +1534,9 @@
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_inet' },
@@ -1443,6 +1554,9 @@
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
 
@@ -1459,6 +1573,9 @@
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'inet_merge' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
@@ -1479,6 +1596,9 @@
 { amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
@@ -1495,6 +1615,9 @@
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '11', amproc => 'hashbpchar' },
 
@@ -1510,6 +1633,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time without time zone
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
@@ -1527,6 +1653,9 @@
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_time' },
@@ -1544,6 +1673,9 @@
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
 
@@ -1560,6 +1692,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '1',
@@ -1573,6 +1708,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1',
@@ -1585,6 +1723,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
@@ -1602,6 +1743,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1621,6 +1765,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1640,6 +1787,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_date' },
@@ -1660,6 +1810,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1679,6 +1832,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1695,6 +1851,9 @@
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1711,6 +1870,9 @@
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi interval
 { amprocfamily => 'brin/interval_minmax_multi_ops',
@@ -1728,6 +1890,9 @@
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_interval' },
@@ -1748,6 +1913,9 @@
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '11', amproc => 'interval_hash' },
 
@@ -1764,6 +1932,9 @@
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time with time zone
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
@@ -1781,6 +1952,9 @@
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_timetz' },
@@ -1800,6 +1974,9 @@
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11', amproc => 'timetz_hash' },
 
@@ -1814,6 +1991,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
@@ -1828,6 +2008,9 @@
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
@@ -1842,6 +2025,9 @@
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi numeric
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
@@ -1859,6 +2045,9 @@
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_numeric' },
@@ -1879,6 +2068,9 @@
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11', amproc => 'hash_numeric' },
 
@@ -1894,6 +2086,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi uuid
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
@@ -1911,6 +2106,9 @@
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_uuid' },
@@ -1928,6 +2126,9 @@
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
 
@@ -1944,6 +2145,9 @@
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '11',
   amproc => 'range_merge(anyrange,anyrange)' },
@@ -1967,6 +2171,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi pg_lsn
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
@@ -1984,6 +2191,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_pg_lsn' },
@@ -2003,6 +2213,9 @@
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11', amproc => 'pg_lsn_hash' },
 
@@ -2019,6 +2232,9 @@
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '11', amproc => 'bound_box' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d4650947c63..fb1fa1581b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8904,6 +8904,10 @@
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
+{ oid => '9637', descr => 'BRIN minmax support',
+  proname => 'brin_minmax_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_within_range' },
 
 # BRIN minmax multi
 { oid => '4616', descr => 'BRIN multi minmax support',
@@ -8925,6 +8929,10 @@
   proname => 'brin_minmax_multi_options', proisstrict => 'f',
   prorettype => 'void', proargtypes => 'internal',
   prosrc => 'brin_minmax_multi_options' },
+{ oid => '9638', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_within_range' },
 
 { oid => '4621', descr => 'BRIN multi minmax int2 distance',
   proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
@@ -9011,6 +9019,10 @@
   proname => 'brin_inclusion_union', prorettype => 'bool',
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
+{ oid => '9639', descr => 'BRIN inclusion support',
+  proname => 'brin_inclusion_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_inclusion_within_range' },
 
 # BRIN bloom
 { oid => '4591', descr => 'BRIN bloom support',
@@ -9030,6 +9042,10 @@
 { oid => '4595', descr => 'BRIN bloom support',
   proname => 'brin_bloom_options', proisstrict => 'f', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+{ oid => '9640', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_within_range' },
 
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
-- 
2.43.0

#11Álvaro Herrera
alvherre@kurilemu.de
In reply to: Arseniy Mukhin (#10)
Re: amcheck support for BRIN indexes

On 2025-Jul-06, Arseniy Mukhin wrote:

Sorry, forget to run a full test run with the new patch version. Some
tests were unhappy with the new unknown support function. Here the new
version with the fix.

Hello, I think this patch is probably a good idea. I don't think it
makes sense to introduce a bunch of code in 0003 only to rewrite it
completely in 0005. I would ask that you re-split your WITHIN_RANGE
(0004) to appear before the amcheck code, and then write the amcheck
code using that new functionality.

/*
* Return a tuple descriptor used for on-disk storage of BRIN tuples.
*/
-static TupleDesc
+TupleDesc
brtuple_disk_tupdesc(BrinDesc *brdesc)

I think we should give this function a better name if it's going to be
exported. How about brin_tuple_tupdesc? (in brin_tuple.h we
seem to distinguish "brin tuples" which are the stored ones, from "brin
mem tuples" which are the ones to be used in memory.)

I didn't read the other patches.

Thanks

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"I'm impressed how quickly you are fixing this obscure issue. I came from
MS SQL and it would be hard for me to put into words how much of a better job
you all are doing on [PostgreSQL]."
Steve Midgley, http://archives.postgresql.org/pgsql-sql/2008-08/msg00000.php

#12Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Álvaro Herrera (#11)
4 attachment(s)
Re: amcheck support for BRIN indexes

On Sun, Jul 6, 2025 at 10:49 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-06, Arseniy Mukhin wrote:

Sorry, forget to run a full test run with the new patch version. Some
tests were unhappy with the new unknown support function. Here the new
version with the fix.

Hello, I think this patch is probably a good idea. I don't think it
makes sense to introduce a bunch of code in 0003 only to rewrite it
completely in 0005. I would ask that you re-split your WITHIN_RANGE
(0004) to appear before the amcheck code, and then write the amcheck
code using that new functionality.

Hi, Álvaro!

Thank you for looking into this.

OK, we can easily revert to the version with consistent function if
needed, so let's get rid of it.

/*
* Return a tuple descriptor used for on-disk storage of BRIN tuples.
*/
-static TupleDesc
+TupleDesc
brtuple_disk_tupdesc(BrinDesc *brdesc)

I think we should give this function a better name if it's going to be
exported. How about brin_tuple_tupdesc? (in brin_tuple.h we
seem to distinguish "brin tuples" which are the stored ones, from "brin
mem tuples" which are the ones to be used in memory.)

'brin_tuple_tupdesc' sounds good to me. Done.

So here is a new version. 0001, 0002 - index structure check. 0003,
0004 - all heap indexed using WITHIN_RANGE approach.

Thank you!

Best regards,
Arseniy Mukhin

Attachments:

v7-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v7-0001-brin-refactoring.patchDownload
From 98c3c70aeeb4628e14ad856f63c5d1fb91d08f21 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v7 1/4] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern and renames it to 'brin_tuple_tupdesc'.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 10 +++++-----
 src/backend/access/common/reloptions.c |  3 ++-
 src/include/access/brin.h              |  1 +
 src/include/access/brin_tuple.h        |  2 ++
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..fc67a708dda 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,8 +57,8 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
-brtuple_disk_tupdesc(BrinDesc *brdesc)
+TupleDesc
+brin_tuple_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
 	if (brdesc->bd_disktdesc == NULL)
@@ -280,7 +280,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 
 	len = hoff = MAXALIGN(len);
 
-	data_len = heap_compute_data_size(brtuple_disk_tupdesc(brdesc),
+	data_len = heap_compute_data_size(brin_tuple_tupdesc(brdesc),
 									  values, nulls);
 	len += data_len;
 
@@ -299,7 +299,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 	 * need to pass a valid null bitmap so that it will correctly skip
 	 * outputting null attributes in the data area.
 	 */
-	heap_fill_tuple(brtuple_disk_tupdesc(brdesc),
+	heap_fill_tuple(brin_tuple_tupdesc(brdesc),
 					values,
 					nulls,
 					(char *) rettuple + hoff,
@@ -682,7 +682,7 @@ brin_deconstruct_tuple(BrinDesc *brdesc,
 	 * may reuse attribute entries for more than one column, we cannot cache
 	 * offsets here.
 	 */
-	diskdsc = brtuple_disk_tupdesc(brdesc);
+	diskdsc = brin_tuple_tupdesc(brdesc);
 	stored = 0;
 	off = 0;
 	for (attnum = 0; attnum < brdesc->bd_tupdesc->natts; attnum++)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..2a12ab03c43 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brin_tuple_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v7-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v7-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From b7855d3f7a5e35d5feb8bf64421fbd893d1697c4 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v7 2/4] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 101 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 ++++++++
 contrib/amcheck/verify_brin.c           | 855 ++++++++++++++++++++++++
 8 files changed, 1407 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..9ec046bb1cf
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regular_pages_check boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..e5fc52ed747
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..b36af37fe03
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,101 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..e9327f2f895
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,855 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regular_pages_check;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regular_pages_check = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+	check_regular_pages(state);
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brin_tuple_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	if (!state->regular_pages_check)
+	{
+		return;
+	}
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v7-0003-Adds-new-BRIN-support-function-withinRange.patchtext/x-patch; charset=US-ASCII; name=v7-0003-Adds-new-BRIN-support-function-withinRange.patchDownload
From 3a9993ad41981187adab2a8ef6471ca422f4aff3 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sat, 5 Jul 2025 23:10:55 +0300
Subject: [PATCH v7 3/4] Adds new BRIN support function 'withinRange'

There is no straightforward way to say if some indexed value is covered
by the range value or not. The new support function provides such a
functionality. Commit adds implementations for all core BRIN
opclasses: minmax, minmax_multi, bloom, inclusion.
---
 src/backend/access/brin/brin_bloom.c        |  44 ++++
 src/backend/access/brin/brin_inclusion.c    |  68 ++++++
 src/backend/access/brin/brin_minmax.c       |  57 ++++++
 src/backend/access/brin/brin_minmax_multi.c | 136 ++++++++----
 src/backend/access/brin/brin_validate.c     |   1 +
 src/include/access/brin_internal.h          |  13 +-
 src/include/catalog/pg_amproc.dat           | 216 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat             |  16 ++
 8 files changed, 510 insertions(+), 41 deletions(-)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index 82b425ce37d..4fa5e39f0ac 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -584,6 +584,50 @@ brin_bloom_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_bloom_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		contains;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then check if bloom filter contains the value.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, val));
+
+	contains = bloom_contains_value(filter, hashValue);
+
+	PG_RETURN_BOOL(contains);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's bloom
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index b86ca5744a3..0b69da3de91 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -237,6 +237,74 @@ brin_inclusion_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(true);
 }
 
+/*
+ * If the passed value is outside the inclusion range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_inclusion_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *finfo;
+	bool		within_range;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Consistent function returns TRUE for any value if the range contains
+	 * unmergeable values. We follow the same logic here.
+	 */
+	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
+		PG_RETURN_BOOL(true);
+
+	/*
+	 * If the opclass supports the concept of empty values, test the passed
+	 * value for emptiness
+	 */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_EMPTY, true);
+	if (finfo != NULL && DatumGetBool(FunctionCall1Coll(finfo, colloid, newval)))
+	{
+		/* Value is empty but the range doesn't contain empty element */
+		if (!DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]))
+		{
+			PG_RETURN_BOOL(false);
+		}
+
+		/* Value is empty and the range contains empty element */
+		PG_RETURN_BOOL(true);
+	}
+
+	/* Use contains function to check if the range contains the value */
+	finfo = inclusion_get_procinfo(bdesc, attno, PROCNUM_CONTAINS, true);
+
+	/* Contains function is optional, but this implementation needs it */
+	if (finfo == NULL)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("The operator class is missing support function %d for column %d.",
+						PROCNUM_CONTAINS, attno)));
+	}
+
+	within_range = DatumGetBool(FunctionCall2Coll(finfo, colloid,
+												  column->bv_values[INCLUSION_UNION],
+												  newval));
+	PG_RETURN_BOOL(within_range);
+}
+
 /*
  * BRIN inclusion consistent function
  *
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index d21ab3a668c..37a5dd103de 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -124,6 +124,63 @@ brin_minmax_add_value(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(updated);
 }
 
+/*
+ * If the passed value is outside the min/max range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	FmgrInfo   *cmpFn;
+	Datum		compar;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is less than the range minimum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTLessStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[0]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Check if the values is greater than the range maximum. */
+
+	cmpFn = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+										 BTGreaterStrategyNumber);
+
+	compar = FunctionCall2Coll(cmpFn, colloid, val, column->bv_values[1]);
+	if (DatumGetBool(compar))
+	{
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * The value is greater than / equals the minimum and is less than /
+	 * equals the maximum so it's within the range
+	 */
+	PG_RETURN_BOOL(true);
+}
+
 /*
  * Given an index tuple corresponding to a certain page range and a scan key,
  * return whether the scan key is consistent with the index tuple's min/max
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 0d1507a2a36..2141c47bc71 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -270,6 +270,9 @@ typedef struct compare_context
 static int	compare_values(const void *a, const void *b, void *arg);
 
 
+static Ranges *deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+									   AttrNumber attno);
+
 #ifdef USE_ASSERT_CHECKING
 /*
  * Check that the order of the array values is correct, using the cmp
@@ -2421,7 +2424,6 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	Form_pg_attribute attr;
 	AttrNumber	attno;
 	Ranges	   *ranges;
-	SerializedRanges *serialized = NULL;
 
 	Assert(!isnull);
 
@@ -2489,55 +2491,119 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
 	}
 	else if (!ranges)
 	{
-		MemoryContext oldctx;
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
+	}
 
-		int			maxvalues;
-		BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the updated summary later.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
 
-		oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	PG_RETURN_BOOL(modified);
+}
 
-		/*
-		 * Determine the insert buffer size - we use 10x the target, capped to
-		 * the maximum number of values in the heap range. This is more than
-		 * enough, considering the actual number of rows per page is likely
-		 * much lower, but meh.
-		 */
-		maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
-						MaxHeapTuplesPerPage * pagesPerRange);
 
-		/* but always at least the original value */
-		maxvalues = Max(maxvalues, serialized->maxvalues);
+/*
+ * Deserialize range value and save it in bdesc->bv_mem_value for future use
+ */
+Ranges *
+deserialize_range_value(BrinDesc *bdesc, BrinValues *column, Oid colloid, const FormData_pg_attribute *attr,
+						AttrNumber attno)
+{
+	MemoryContext oldctx;
+	SerializedRanges *serialized = NULL;
+	Ranges	   *ranges;
 
-		/* always cap by MIN/MAX */
-		maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
-		maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+	int			maxvalues;
+	BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index);
 
-		ranges = brin_range_deserialize(maxvalues, serialized);
+	oldctx = MemoryContextSwitchTo(column->bv_context);
 
-		ranges->attno = attno;
-		ranges->colloid = colloid;
-		ranges->typid = attr->atttypid;
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
 
-		/* we'll certainly need the comparator, so just look it up now */
-		ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
-														 BTLessStrategyNumber);
+	/*
+	 * Determine the insert buffer size - we use 10x the target, capped to the
+	 * maximum number of values in the heap range. This is more than enough,
+	 * considering the actual number of rows per page is likely much lower,
+	 * but meh.
+	 */
+	maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR,
+					MaxHeapTuplesPerPage * pagesPerRange);
 
-		column->bv_mem_value = PointerGetDatum(ranges);
-		column->bv_serialize = brin_minmax_multi_serialize;
+	/* but always at least the original value */
+	maxvalues = Max(maxvalues, serialized->maxvalues);
 
-		MemoryContextSwitchTo(oldctx);
+	/* always cap by MIN/MAX */
+	maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN);
+	maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX);
+
+	ranges = brin_range_deserialize(maxvalues, serialized);
+
+	ranges->attno = attno;
+	ranges->colloid = colloid;
+	ranges->typid = attr->atttypid;
+
+	/* we'll certainly need the comparator, so just look it up now */
+	ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+													 BTLessStrategyNumber);
+
+	column->bv_mem_value = PointerGetDatum(ranges);
+	column->bv_serialize = brin_minmax_multi_serialize;
+
+	MemoryContextSwitchTo(oldctx);
+
+	return ranges;
+}
+
+/*
+ * If the passed value is outside the minmax_multi range return false.
+ * Otherwise, return true.
+ */
+Datum
+brin_minmax_multi_within_range(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		val = PG_GETARG_DATUM(2);
+	bool		isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		contains = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges	   *ranges;
+	FmgrInfo   *cmpFn;
+
+	Assert(!isnull);
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/* use the already deserialized value, if possible */
+	ranges = (Ranges *) DatumGetPointer(column->bv_mem_value);
+
+	/* The range is empty, return false */
+	if (column->bv_allnulls)
+	{
+		PG_RETURN_BOOL(false);
+	}
+	else if (!ranges)
+	{
+		ranges = deserialize_range_value(bdesc, column, colloid, attr, attno);
 	}
 
-	/*
-	 * Try to add the new value to the range. We need to update the modified
-	 * flag, so that we serialize the updated summary later.
-	 */
-	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
 
+	/* comprehensive checks of the input ranges */
+	AssertCheckRanges(ranges, cmpFn, colloid);
 
-	PG_RETURN_BOOL(modified);
+	/* Use 'full = true' here, as we don't want any false negatives */
+	contains = range_contains_value(bdesc, colloid, attno, attr, ranges, val, true);
+
+	PG_RETURN_BOOL(contains);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 915b8628b46..2c59d7ecca5 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -84,6 +84,7 @@ brinvalidate(Oid opclassoid)
 											1, 1, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_ADDVALUE:
+			case BRIN_PROCNUM_WITHINRANGE:
 				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
 											4, 4, INTERNALOID, INTERNALOID,
 											INTERNALOID, INTERNALOID);
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index d093a0bf130..5df87761cf1 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -67,12 +67,13 @@ typedef struct BrinDesc
  * opclasses can define more function support numbers, which must fall into
  * BRIN_FIRST_OPTIONAL_PROCNUM .. BRIN_LAST_OPTIONAL_PROCNUM.
  */
-#define BRIN_PROCNUM_OPCINFO		1
-#define BRIN_PROCNUM_ADDVALUE		2
-#define BRIN_PROCNUM_CONSISTENT		3
-#define BRIN_PROCNUM_UNION			4
-#define BRIN_MANDATORY_NPROCS		4
-#define BRIN_PROCNUM_OPTIONS 		5	/* optional */
+#define BRIN_PROCNUM_OPCINFO		    1
+#define BRIN_PROCNUM_ADDVALUE		    2
+#define BRIN_PROCNUM_CONSISTENT		    3
+#define BRIN_PROCNUM_UNION			    4
+#define BRIN_MANDATORY_NPROCS		    4
+#define BRIN_PROCNUM_OPTIONS 		    5	/* optional */
+#define BRIN_PROCNUM_WITHINRANGE 		6	/* optional */
 /* procedure numbers up to 10 are reserved for BRIN future expansion */
 #define BRIN_FIRST_OPTIONAL_PROCNUM 11
 #define BRIN_LAST_OPTIONAL_PROCNUM	15
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa..c3947bbc410 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -847,6 +847,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bytea_minmax_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom bytea
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
@@ -863,6 +866,9 @@
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
+  amprocrighttype => 'bytea', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bytea_bloom_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '11', amproc => 'hashbytea' },
 
@@ -878,6 +884,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/char_minmax_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom "char"
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
@@ -892,6 +901,9 @@
   amprocrighttype => 'char', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
+  amprocrighttype => 'char', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/char_bloom_ops', amproclefttype => 'char',
   amprocrighttype => 'char', amprocnum => '11', amproc => 'hashchar' },
 
@@ -907,6 +919,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/name_minmax_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom name
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
@@ -921,6 +936,9 @@
   amprocrighttype => 'name', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
+  amprocrighttype => 'name', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/name_bloom_ops', amproclefttype => 'name',
   amprocrighttype => 'name', amprocnum => '11', amproc => 'hashname' },
 
@@ -936,6 +954,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '1',
@@ -948,6 +969,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '1',
@@ -960,6 +984,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/integer_minmax_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi integer: int2, int4, int8
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
@@ -977,6 +1004,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int2' },
@@ -996,6 +1026,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1015,6 +1048,9 @@
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/integer_minmax_multi_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int8' },
@@ -1032,6 +1068,9 @@
   amprocrighttype => 'int8', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
+  amprocrighttype => 'int8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int8',
   amprocrighttype => 'int8', amprocnum => '11', amproc => 'hashint8' },
 
@@ -1047,6 +1086,9 @@
   amprocrighttype => 'int2', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
+  amprocrighttype => 'int2', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int2',
   amprocrighttype => 'int2', amprocnum => '11', amproc => 'hashint2' },
 
@@ -1062,6 +1104,9 @@
   amprocrighttype => 'int4', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
+  amprocrighttype => 'int4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/integer_bloom_ops', amproclefttype => 'int4',
   amprocrighttype => 'int4', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1077,6 +1122,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/text_minmax_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom text
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
@@ -1091,6 +1139,9 @@
   amprocrighttype => 'text', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
+  amprocrighttype => 'text', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/text_bloom_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '11', amproc => 'hashtext' },
 
@@ -1105,6 +1156,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/oid_minmax_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi oid
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
@@ -1122,6 +1176,9 @@
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_int4' },
@@ -1139,6 +1196,9 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
+  amprocrighttype => 'oid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '11', amproc => 'hashoid' },
 
@@ -1153,6 +1213,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/tid_minmax_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom tid
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
@@ -1167,6 +1230,9 @@
   amprocrighttype => 'tid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/tid_bloom_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11', amproc => 'hashtid' },
 
@@ -1186,6 +1252,9 @@
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
+  amprocrighttype => 'tid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/tid_minmax_multi_ops', amproclefttype => 'tid',
   amprocrighttype => 'tid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_tid' },
@@ -1203,6 +1272,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '1',
@@ -1216,6 +1288,9 @@
 { amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/float_minmax_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi float
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
@@ -1233,6 +1308,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float4' },
@@ -1252,6 +1330,9 @@
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/float_minmax_multi_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_float8' },
@@ -1271,6 +1352,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
+  amprocrighttype => 'float4', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float4',
   amprocrighttype => 'float4', amprocnum => '11', amproc => 'hashfloat4' },
 
@@ -1288,6 +1372,9 @@
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
+  amprocrighttype => 'float8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/float_bloom_ops', amproclefttype => 'float8',
   amprocrighttype => 'float8', amprocnum => '11', amproc => 'hashfloat8' },
 
@@ -1304,6 +1391,9 @@
 { amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr_minmax_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
@@ -1321,6 +1411,9 @@
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr_minmax_multi_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_macaddr' },
@@ -1341,6 +1434,9 @@
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
+  amprocrighttype => 'macaddr', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr_bloom_ops', amproclefttype => 'macaddr',
   amprocrighttype => 'macaddr', amprocnum => '11', amproc => 'hashmacaddr' },
 
@@ -1357,6 +1453,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/macaddr8_minmax_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi macaddr8
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
@@ -1374,6 +1473,9 @@
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/macaddr8_minmax_multi_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/macaddr8_minmax_multi_ops',
   amproclefttype => 'macaddr8', amprocrighttype => 'macaddr8',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_macaddr8' },
@@ -1394,6 +1496,9 @@
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
+  amprocrighttype => 'macaddr8', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/macaddr8_bloom_ops', amproclefttype => 'macaddr8',
   amprocrighttype => 'macaddr8', amprocnum => '11', amproc => 'hashmacaddr8' },
 
@@ -1409,6 +1514,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/network_minmax_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi inet
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
@@ -1426,6 +1534,9 @@
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/network_minmax_multi_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_inet' },
@@ -1443,6 +1554,9 @@
   amprocrighttype => 'inet', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/network_bloom_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'hashinet' },
 
@@ -1459,6 +1573,9 @@
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '11', amproc => 'inet_merge' },
 { amprocfamily => 'brin/network_inclusion_ops', amproclefttype => 'inet',
@@ -1479,6 +1596,9 @@
 { amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bpchar_minmax_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # bloom character
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
@@ -1495,6 +1615,9 @@
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
+  amprocrighttype => 'bpchar', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/bpchar_bloom_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '11', amproc => 'hashbpchar' },
 
@@ -1510,6 +1633,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/time_minmax_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time without time zone
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
@@ -1527,6 +1653,9 @@
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/time_minmax_multi_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_time' },
@@ -1544,6 +1673,9 @@
   amprocrighttype => 'time', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
+  amprocrighttype => 'time', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/time_bloom_ops', amproclefttype => 'time',
   amprocrighttype => 'time', amprocnum => '11', amproc => 'time_hash' },
 
@@ -1560,6 +1692,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '1',
@@ -1573,6 +1708,9 @@
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '1',
@@ -1585,6 +1723,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/datetime_minmax_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi datetime (date, timestamp, timestamptz)
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
@@ -1602,6 +1743,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamp', amprocrighttype => 'timestamp',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1621,6 +1765,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '5', amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops',
   amproclefttype => 'timestamptz', amprocrighttype => 'timestamptz',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_timestamp' },
@@ -1640,6 +1787,9 @@
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/datetime_minmax_multi_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_date' },
@@ -1660,6 +1810,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
+  amprocrighttype => 'timestamp', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamp',
   amprocrighttype => 'timestamp', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1679,6 +1832,9 @@
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
+  amprocrighttype => 'timestamptz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'timestamptz',
   amprocrighttype => 'timestamptz', amprocnum => '11',
   amproc => 'timestamp_hash' },
@@ -1695,6 +1851,9 @@
   amprocrighttype => 'date', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
+  amprocrighttype => 'date', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/datetime_bloom_ops', amproclefttype => 'date',
   amprocrighttype => 'date', amprocnum => '11', amproc => 'hashint4' },
 
@@ -1711,6 +1870,9 @@
 { amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/interval_minmax_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi interval
 { amprocfamily => 'brin/interval_minmax_multi_ops',
@@ -1728,6 +1890,9 @@
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/interval_minmax_multi_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/interval_minmax_multi_ops',
   amproclefttype => 'interval', amprocrighttype => 'interval',
   amprocnum => '11', amproc => 'brin_minmax_multi_distance_interval' },
@@ -1748,6 +1913,9 @@
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
+  amprocrighttype => 'interval', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/interval_bloom_ops', amproclefttype => 'interval',
   amprocrighttype => 'interval', amprocnum => '11', amproc => 'interval_hash' },
 
@@ -1764,6 +1932,9 @@
 { amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/timetz_minmax_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi time with time zone
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
@@ -1781,6 +1952,9 @@
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/timetz_minmax_multi_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_timetz' },
@@ -1800,6 +1974,9 @@
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
+  amprocrighttype => 'timetz', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/timetz_bloom_ops', amproclefttype => 'timetz',
   amprocrighttype => 'timetz', amprocnum => '11', amproc => 'timetz_hash' },
 
@@ -1814,6 +1991,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
   amprocrighttype => 'bit', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/bit_minmax_ops', amproclefttype => 'bit',
+  amprocrighttype => 'bit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax bit varying
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
@@ -1828,6 +2008,9 @@
 { amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
   amprocrighttype => 'varbit', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/varbit_minmax_ops', amproclefttype => 'varbit',
+  amprocrighttype => 'varbit', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax numeric
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
@@ -1842,6 +2025,9 @@
 { amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/numeric_minmax_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi numeric
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
@@ -1859,6 +2045,9 @@
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/numeric_minmax_multi_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_numeric' },
@@ -1879,6 +2068,9 @@
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
+  amprocrighttype => 'numeric', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/numeric_bloom_ops', amproclefttype => 'numeric',
   amprocrighttype => 'numeric', amprocnum => '11', amproc => 'hash_numeric' },
 
@@ -1894,6 +2086,9 @@
   amproc => 'brin_minmax_consistent' },
 { amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/uuid_minmax_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi uuid
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
@@ -1911,6 +2106,9 @@
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/uuid_minmax_multi_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_uuid' },
@@ -1928,6 +2126,9 @@
   amprocrighttype => 'uuid', amprocnum => '4', amproc => 'brin_bloom_union' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '5', amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
+  amprocrighttype => 'uuid', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/uuid_bloom_ops', amproclefttype => 'uuid',
   amprocrighttype => 'uuid', amprocnum => '11', amproc => 'uuid_hash' },
 
@@ -1944,6 +2145,9 @@
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/range_inclusion_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '11',
   amproc => 'range_merge(anyrange,anyrange)' },
@@ -1967,6 +2171,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '4',
   amproc => 'brin_minmax_union' },
+{ amprocfamily => 'brin/pg_lsn_minmax_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_within_range' },
 
 # minmax multi pg_lsn
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
@@ -1984,6 +2191,9 @@
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_minmax_multi_options' },
+{ amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_minmax_multi_within_range' },
 { amprocfamily => 'brin/pg_lsn_minmax_multi_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11',
   amproc => 'brin_minmax_multi_distance_pg_lsn' },
@@ -2003,6 +2213,9 @@
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '5',
   amproc => 'brin_bloom_options' },
+{ amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
+  amprocrighttype => 'pg_lsn', amprocnum => '6',
+  amproc => 'brin_bloom_within_range' },
 { amprocfamily => 'brin/pg_lsn_bloom_ops', amproclefttype => 'pg_lsn',
   amprocrighttype => 'pg_lsn', amprocnum => '11', amproc => 'pg_lsn_hash' },
 
@@ -2019,6 +2232,9 @@
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '4',
   amproc => 'brin_inclusion_union' },
+{ amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '6',
+  amproc => 'brin_inclusion_within_range' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '11', amproc => 'bound_box' },
 { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d4650947c63..fb1fa1581b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8904,6 +8904,10 @@
 { oid => '3386', descr => 'BRIN minmax support',
   proname => 'brin_minmax_union', prorettype => 'bool',
   proargtypes => 'internal internal internal', prosrc => 'brin_minmax_union' },
+{ oid => '9637', descr => 'BRIN minmax support',
+  proname => 'brin_minmax_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_within_range' },
 
 # BRIN minmax multi
 { oid => '4616', descr => 'BRIN multi minmax support',
@@ -8925,6 +8929,10 @@
   proname => 'brin_minmax_multi_options', proisstrict => 'f',
   prorettype => 'void', proargtypes => 'internal',
   prosrc => 'brin_minmax_multi_options' },
+{ oid => '9638', descr => 'BRIN multi minmax support',
+  proname => 'brin_minmax_multi_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_minmax_multi_within_range' },
 
 { oid => '4621', descr => 'BRIN multi minmax int2 distance',
   proname => 'brin_minmax_multi_distance_int2', prorettype => 'float8',
@@ -9011,6 +9019,10 @@
   proname => 'brin_inclusion_union', prorettype => 'bool',
   proargtypes => 'internal internal internal',
   prosrc => 'brin_inclusion_union' },
+{ oid => '9639', descr => 'BRIN inclusion support',
+  proname => 'brin_inclusion_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_inclusion_within_range' },
 
 # BRIN bloom
 { oid => '4591', descr => 'BRIN bloom support',
@@ -9030,6 +9042,10 @@
 { oid => '4595', descr => 'BRIN bloom support',
   proname => 'brin_bloom_options', proisstrict => 'f', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'brin_bloom_options' },
+{ oid => '9640', descr => 'BRIN bloom support',
+  proname => 'brin_bloom_within_range', prorettype => 'bool',
+  proargtypes => 'internal internal internal internal',
+  prosrc => 'brin_bloom_within_range' },
 
 # userlock replacements
 { oid => '2880', descr => 'obtain exclusive advisory lock',
-- 
2.43.0

v7-0004-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v7-0004-amcheck-brin_index_check-heap-all-indexed.patchDownload
From 744f6b4b1d317b77ccadae0eefe8a08065f48d4c Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:41:34 +0300
Subject: [PATCH v7 4/4] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
'heap all indexed' check: we validate every index range tuple
against every heap tuple within the range using withinRange function.
Also, we check that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   5 +-
 contrib/amcheck/expected/check_brin.out |  22 +-
 contrib/amcheck/sql/check_brin.sql      |  22 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 +++-
 contrib/amcheck/verify_brin.c           | 313 +++++++++++++++++++++++-
 5 files changed, 377 insertions(+), 36 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 9ec046bb1cf..d4f44495bba 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,12 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regular_pages_check boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index e5fc52ed747..d8898f47fbe 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -19,7 +19,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -28,7 +28,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index b36af37fe03..b4137799c57 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -17,12 +17,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -34,12 +34,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -51,12 +51,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -68,12 +68,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -87,12 +87,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index e9327f2f895..a446a210030 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -38,7 +38,8 @@ typedef struct BrinCheckState
 
 	/* Check arguments */
 
-	bool		regular_pages_check;
+	bool		regularpagescheck;
+	bool		heapallindexed;
 
 	/* BRIN check common fields */
 
@@ -67,6 +68,25 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *withinRangeFn;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +107,17 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +126,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -102,7 +134,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
-	state->regular_pages_check = PG_GETARG_BOOL(1);
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +160,12 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
-
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -160,8 +196,13 @@ check_brin_index_structure(BrinCheckState * state)
 	/* Check revmap first, blocks: [1, lastRevmapPage] */
 	check_revmap(state);
 
-	/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
-	check_regular_pages(state);
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
 }
 
 /* Meta page check and save some data for the further check */
@@ -614,11 +655,6 @@ check_regular_pages(BrinCheckState * state)
 	ReadStreamBlockNumberCB stream_cb;
 	BlockRangeReadStreamPrivate stream_data;
 
-	if (!state->regular_pages_check)
-	{
-		return;
-	}
-
 	/* reset state */
 	state->revmapBlk = InvalidBlockNumber;
 	state->revmapbuf = InvalidBuffer;
@@ -628,7 +664,6 @@ check_regular_pages(BrinCheckState * state)
 	state->regpageoffset = InvalidOffsetNumber;
 	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
 
-
 	/*
 	 * Prepare stream data for regular pages walk. It is safe to use batchmode
 	 * as block_range_read_stream_cb takes no locks.
@@ -788,6 +823,246 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * We use withinRange function for every nonnull value (in case
+ * of io_regular_nulls = false we use withinRange function for null values too).
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->withinRangeFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/* Prepare withinRange function for each attribute */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		if (RegProcedureIsValid(index_getprocid(state->idxrel, attno, BRIN_PROCNUM_WITHINRANGE)))
+		{
+			FmgrInfo   *fn = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_WITHINRANGE);
+
+			fmgr_info_copy(&state->withinRangeFn[attno - 1], fn, CurrentMemoryContext);
+		}
+		else
+		{
+			/*
+			 * If there is no withinRange function for the attribute, notice
+			 * user about it. We continue heap all indexed check even for
+			 * indexes with just one attribute as even without support
+			 * function some errors could be detected.
+			 */
+			ereport(NOTICE,
+					errmsg("Missing support function %d for attribute %d of index \"%s\". "
+						   "Skip heap all indexed check for this attribute.",
+						   BRIN_PROCNUM_WITHINRANGE,
+						   attno,
+						   RelationGetRelationName(state->idxrel)
+						   ));
+		}
+
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			all_consist_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is true or not,
+ * We use withinRangeFn for all nonnull values and null values if io_regular_nulls = false.
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		withinRangeFnResult;
+		bool		withinRange;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		/*
+		 * Use hasnulls flag for oi_regular_nulls is true. Otherwise, delegate
+		 * check to withinRangeFn
+		 */
+		if (nulls[attindex] && oi_regular_nulls)
+		{
+			/* We have null value, so hasnulls or allnulls must be true */
+			if (!(bval->bv_hasnulls || bval->bv_allnulls))
+			{
+				all_consist_ereport(state, tid, "range hasnulls and allnulls are false, but contains a null value");
+			}
+			continue;
+		}
+
+		/* If we have a nonnull value allnulls should be false */
+		if (!nulls[attindex] && bval->bv_allnulls)
+		{
+			all_consist_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		withinRangeFnResult = FunctionCall4Coll(&state->withinRangeFn[attindex],
+												state->idxrel->rd_indcollation[attindex],
+												PointerGetDatum(bdesc),
+												PointerGetDatum(bval),
+												values[attindex],
+												nulls[attindex]);
+
+		withinRange = DatumGetBool(withinRangeFnResult);
+		if (!withinRange)
+		{
+			all_consist_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -853,3 +1128,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+all_consist_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

#13Tomas Vondra
tomas@vondra.me
In reply to: Arseniy Mukhin (#12)
Re: amcheck support for BRIN indexes

On 7/7/25 13:06, Arseniy Mukhin wrote:

On Sun, Jul 6, 2025 at 10:49 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-06, Arseniy Mukhin wrote:

Sorry, forget to run a full test run with the new patch version. Some
tests were unhappy with the new unknown support function. Here the new
version with the fix.

Hello, I think this patch is probably a good idea. I don't think it
makes sense to introduce a bunch of code in 0003 only to rewrite it
completely in 0005. I would ask that you re-split your WITHIN_RANGE
(0004) to appear before the amcheck code, and then write the amcheck
code using that new functionality.

Hi, Álvaro!

Thank you for looking into this.

OK, we can easily revert to the version with consistent function if
needed, so let's get rid of it.

Alvaro, what's your opinion on the introduction of the new WITHIN_RANGE?
I'd probably try to do this using the regular consistent function:

(a) we don't need to add stuff to all BRIN opclasses to support this

(b) it gives us additional testing of the consistent function

(c) building a scan key for equality seems pretty trivial

What do you think?

--
Tomas Vondra

#14Álvaro Herrera
alvherre@kurilemu.de
In reply to: Tomas Vondra (#13)
Re: amcheck support for BRIN indexes

On 2025-Jul-07, Tomas Vondra wrote:

Alvaro, what's your opinion on the introduction of the new WITHIN_RANGE?
I'd probably try to do this using the regular consistent function:

(a) we don't need to add stuff to all BRIN opclasses to support this

(b) it gives us additional testing of the consistent function

(c) building a scan key for equality seems pretty trivial

What do you think?

Oh yeah, if we can build this on top of the existing primitives, then
I'm all for that.

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/

#15Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Álvaro Herrera (#14)
Re: amcheck support for BRIN indexes

On Mon, Jul 7, 2025 at 3:21 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-07, Tomas Vondra wrote:

Alvaro, what's your opinion on the introduction of the new WITHIN_RANGE?
I'd probably try to do this using the regular consistent function:

(a) we don't need to add stuff to all BRIN opclasses to support this

(b) it gives us additional testing of the consistent function

(c) building a scan key for equality seems pretty trivial

What do you think?

Oh yeah, if we can build this on top of the existing primitives, then
I'm all for that.

Thank you for the feedback! I agree with the benefits. Speaking of
(с), it seems most of the time to be really trivial to build such a
ScanKey, but not every opclass supports '=' operator. amcheck should
handle these cases somehow then. I see two options here. The first is
to not provide 'heap all indexed' check for such opclasses, which is
sad because even one core opclass (box_inclusion_ops) doesn't support
'=' operator, postgis brin opclasses don't support it too AFAICS. The
second option is to let the user define which operator to use during
the check, which, I think, makes user experience much worse in this
case. So both options look not good from the user POV as for me, so I
don't know. What do you think about it?

And should I revert the patchset to the consistent function version then?

Best regards,
Arseniy Mukhin

#16Tomas Vondra
tomas@vondra.me
In reply to: Arseniy Mukhin (#15)
Re: amcheck support for BRIN indexes

On 7/8/25 14:40, Arseniy Mukhin wrote:

On Mon, Jul 7, 2025 at 3:21 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-07, Tomas Vondra wrote:

Alvaro, what's your opinion on the introduction of the new WITHIN_RANGE?
I'd probably try to do this using the regular consistent function:

(a) we don't need to add stuff to all BRIN opclasses to support this

(b) it gives us additional testing of the consistent function

(c) building a scan key for equality seems pretty trivial

What do you think?

Oh yeah, if we can build this on top of the existing primitives, then
I'm all for that.

Thank you for the feedback! I agree with the benefits. Speaking of
(с), it seems most of the time to be really trivial to build such a
ScanKey, but not every opclass supports '=' operator. amcheck should
handle these cases somehow then. I see two options here. The first is
to not provide 'heap all indexed' check for such opclasses, which is
sad because even one core opclass (box_inclusion_ops) doesn't support
'=' operator, postgis brin opclasses don't support it too AFAICS. The
second option is to let the user define which operator to use during
the check, which, I think, makes user experience much worse in this
case. So both options look not good from the user POV as for me, so I
don't know. What do you think about it?

And should I revert the patchset to the consistent function version then?

Yeah, that's a good point. The various opclasses may support different
operators, and we don't know which "strategy" to fill into the scan key.
Minmax needs BTEqualStrategyNumber, inclusion RTContainsStrategyNumber,
and so on.

I wonder if there's a way to figure this out from the catalogs, but I
can't think of anything. Maybe requiring the user to supply the operator
(and not checking heap if it's not provided)

SELECT brin_index_check(..., '@>');

would not be too bad ...

Alvaro, any ideas?

--
Tomas Vondra

#17Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Tomas Vondra (#16)
Re: amcheck support for BRIN indexes

On Tue, Jul 8, 2025 at 4:21 PM Tomas Vondra <tomas@vondra.me> wrote:

On 7/8/25 14:40, Arseniy Mukhin wrote:

On Mon, Jul 7, 2025 at 3:21 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-07, Tomas Vondra wrote:

Alvaro, what's your opinion on the introduction of the new WITHIN_RANGE?
I'd probably try to do this using the regular consistent function:

(a) we don't need to add stuff to all BRIN opclasses to support this

(b) it gives us additional testing of the consistent function

(c) building a scan key for equality seems pretty trivial

What do you think?

Oh yeah, if we can build this on top of the existing primitives, then
I'm all for that.

Thank you for the feedback! I agree with the benefits. Speaking of
(с), it seems most of the time to be really trivial to build such a
ScanKey, but not every opclass supports '=' operator. amcheck should
handle these cases somehow then. I see two options here. The first is
to not provide 'heap all indexed' check for such opclasses, which is
sad because even one core opclass (box_inclusion_ops) doesn't support
'=' operator, postgis brin opclasses don't support it too AFAICS. The
second option is to let the user define which operator to use during
the check, which, I think, makes user experience much worse in this
case. So both options look not good from the user POV as for me, so I
don't know. What do you think about it?

And should I revert the patchset to the consistent function version then?

Yeah, that's a good point. The various opclasses may support different
operators, and we don't know which "strategy" to fill into the scan key.
Minmax needs BTEqualStrategyNumber, inclusion RTContainsStrategyNumber,
and so on.

I wonder if there's a way to figure this out from the catalogs, but I
can't think of anything. Maybe requiring the user to supply the operator
(and not checking heap if it's not provided)

SELECT brin_index_check(..., '@>');

would not be too bad ...

This doesn't change much, but it seems we need an array of operators
in the case of a multi-column index. And one more thing, it's
theoretically possible that opclass doesn't have the operator we need
at all. I'm not aware of such cases, but perhaps in the future
something like GIN tsvector_ops could be created for BRIN.
tsvector_ops doesn't have an operator that could be used here.

Best regards,
Arseniy Mukhin

#18Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#17)
4 attachment(s)
Re: amcheck support for BRIN indexes

Hi,

While reviewing gist amcheck patch [1]/messages/by-id/41F2A10C-4577-413B-9140-BE81CCE04A60@yandex-team.ru I realized that brin amcheck
also must check if current snapshot is OK with index indcheckxmin (as
btree, gist do it). Currently this check is contained in btree amcheck
code, but other AMs need it for heapallindexed as well, so I moved it
from btree to verify_common (0003 patch).

Also I returned a consistentFn approach in heapallindexed as it seems
more preferable. But it's not a big deal to return to the within_range
approach if needed.

[1]: /messages/by-id/41F2A10C-4577-413B-9140-BE81CCE04A60@yandex-team.ru

Best regards,
Arseniy Mukhin

Attachments:

v8-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v8-0001-brin-refactoring.patchDownload
From fdb35875155d9c905901f70a7f967c2cf4bfb5e4 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v8 1/4] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern and renames it to 'brin_tuple_tupdesc'.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 10 +++++-----
 src/backend/access/common/reloptions.c |  3 ++-
 src/include/access/brin.h              |  1 +
 src/include/access/brin_tuple.h        |  2 ++
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..fc67a708dda 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,8 +57,8 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
-brtuple_disk_tupdesc(BrinDesc *brdesc)
+TupleDesc
+brin_tuple_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
 	if (brdesc->bd_disktdesc == NULL)
@@ -280,7 +280,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 
 	len = hoff = MAXALIGN(len);
 
-	data_len = heap_compute_data_size(brtuple_disk_tupdesc(brdesc),
+	data_len = heap_compute_data_size(brin_tuple_tupdesc(brdesc),
 									  values, nulls);
 	len += data_len;
 
@@ -299,7 +299,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 	 * need to pass a valid null bitmap so that it will correctly skip
 	 * outputting null attributes in the data area.
 	 */
-	heap_fill_tuple(brtuple_disk_tupdesc(brdesc),
+	heap_fill_tuple(brin_tuple_tupdesc(brdesc),
 					values,
 					nulls,
 					(char *) rettuple + hoff,
@@ -682,7 +682,7 @@ brin_deconstruct_tuple(BrinDesc *brdesc,
 	 * may reuse attribute entries for more than one column, we cannot cache
 	 * offsets here.
 	 */
-	diskdsc = brtuple_disk_tupdesc(brdesc);
+	diskdsc = brin_tuple_tupdesc(brdesc);
 	stored = 0;
 	off = 0;
 	for (attnum = 0; attnum < brdesc->bd_tupdesc->natts; attnum++)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..2a12ab03c43 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brin_tuple_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v8-0004-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v8-0004-amcheck-brin_index_check-heap-all-indexed.patchDownload
From 176af35834f1c2549b64010fb066586090d085d9 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Jul 2025 18:06:36 +0300
Subject: [PATCH v8 4/4] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
heap_all_indexed check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  22 +-
 contrib/amcheck/sql/check_brin.sql      |  22 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 490 +++++++++++++++++++++++-
 5 files changed, 565 insertions(+), 26 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 0354451c472..6337e065bb1 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regularpagescheck boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index e5fc52ed747..be85c32bc58 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -19,7 +19,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -28,7 +28,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index b36af37fe03..4f16f31c7f8 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -17,12 +17,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -34,12 +34,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -51,12 +51,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -68,12 +68,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -87,12 +87,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index d4024e76b56..b7bf1513734 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -39,6 +39,8 @@ typedef struct BrinCheckState
 	/* Check arguments */
 
 	bool		regularpagescheck;
+	bool		heapallindexed;
+	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -103,6 +147,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
 	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
+	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,27 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heapallindexed)
+	{
+		check_and_prepare_operator_names(state);
+
+		/*
+		 * Check if we are OK with indcheckxmin, and unregister snapshot as we
+		 * don't need it further
+		 */
+		UnregisterSnapshot(RegisterSnapshotAndCheckIndexCheckXMin(state->idxrel));
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -628,7 +692,6 @@ check_regular_pages(BrinCheckState * state)
 	state->regpageoffset = InvalidOffsetNumber;
 	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
 
-
 	/*
 	 * Prepare stream data for regular pages walk. It is safe to use batchmode
 	 * as block_range_read_stream_cb takes no locks.
@@ -788,6 +851,415 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet. Also, we want to support CIC indexes.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			heap_all_indexed_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					heap_all_indexed_ereport(state, tid,
+											 "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				heap_all_indexed_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use nonnull scan key */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			heap_all_indexed_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -853,3 +1325,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

v8-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v8-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From 0c1c8851ba001e2ef45d399189041fc5e539ef24 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v8 2/4] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 101 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 ++++++++
 contrib/amcheck/verify_brin.c           | 855 ++++++++++++++++++++++++
 8 files changed, 1407 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..0354451c472
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regularpagescheck boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..e5fc52ed747
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..b36af37fe03
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,101 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..d4024e76b56
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,855 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regularpagescheck;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brin_tuple_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

v8-0003-amcheck-common_verify-register-snapshot-with-indc.patchtext/x-patch; charset=US-ASCII; name=v8-0003-amcheck-common_verify-register-snapshot-with-indc.patchDownload
From 0a75015e109f5c58db9440215e8a88540c5ed119 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Jul 2025 17:37:13 +0300
Subject: [PATCH v8 3/4] amcheck: common_verify - register snapshot with
 indcheckxmin check

Moves check to common_verify. Every index needs it for heapallindexed check.
---
 contrib/amcheck/verify_common.c | 29 +++++++++++++++++++++++++++++
 contrib/amcheck/verify_common.h |  2 ++
 contrib/amcheck/verify_nbtree.c | 23 +----------------------
 3 files changed, 32 insertions(+), 22 deletions(-)

diff --git a/contrib/amcheck/verify_common.c b/contrib/amcheck/verify_common.c
index a31ce06ed99..bbcefe82898 100644
--- a/contrib/amcheck/verify_common.c
+++ b/contrib/amcheck/verify_common.c
@@ -189,3 +189,32 @@ index_checkable(Relation rel, Oid am_id)
 
 	return amcheck_index_mainfork_expected(rel);
 }
+
+/*
+ * 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.
+ */
+Snapshot
+RegisterSnapshotAndCheckIndexCheckXMin(Relation idxrel)
+{
+	Snapshot	snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
+	if (IsolationUsesXactSnapshot() && idxrel->rd_index->indcheckxmin &&
+		!TransactionIdPrecedes(HeapTupleHeaderGetXmin(idxrel->rd_indextuple->t_data),
+							   snapshot->xmin))
+		ereport(ERROR,
+				(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+				 errmsg("index \"%s\" cannot be verified using transaction snapshot",
+						RelationGetRelationName(idxrel))));
+
+	return snapshot;
+}
diff --git a/contrib/amcheck/verify_common.h b/contrib/amcheck/verify_common.h
index 3f4c57f963d..7679e33da77 100644
--- a/contrib/amcheck/verify_common.h
+++ b/contrib/amcheck/verify_common.h
@@ -26,3 +26,5 @@ extern void amcheck_lock_relation_and_check(Oid indrelid,
 											Oid am_id,
 											IndexDoCheckCallback check,
 											LOCKMODE lockmode, void *state);
+
+extern Snapshot RegisterSnapshotAndCheckIndexCheckXMin(Relation idxrel);
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 0949c88983a..c9aa9044045 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -441,28 +441,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		if (!state->readonly)
 		{
-			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),
-									   snapshot->xmin))
-				ereport(ERROR,
-						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
-						 errmsg("index \"%s\" cannot be verified using transaction snapshot",
-								RelationGetRelationName(rel))));
+			snapshot = RegisterSnapshotAndCheckIndexCheckXMin(state->rel);
 		}
 	}
 
-- 
2.43.0

#19Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#18)
Re: amcheck support for BRIN indexes

Hi,

On Tue, Jul 22, 2025 at 6:43 PM Arseniy Mukhin
<arseniy.mukhin.dev@gmail.com> wrote:

Hi,

While reviewing gist amcheck patch [1] I realized that brin amcheck
also must check if current snapshot is OK with index indcheckxmin (as
btree, gist do it). Currently this check is contained in btree amcheck
code, but other AMs need it for heapallindexed as well, so I moved it
from btree to verify_common (0003 patch).

There was a compiler warning on CI, so there is a new version with the
fix (adds #include to verify_common).
I also moved it to PG19-2.

Best regards,
Arseniy Mukhin

#20Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Arseniy Mukhin (#19)
4 attachment(s)
Re: amcheck support for BRIN indexes

On Fri, Aug 1, 2025 at 11:22 PM Arseniy Mukhin
<arseniy.mukhin.dev@gmail.com> wrote:

Hi,

On Tue, Jul 22, 2025 at 6:43 PM Arseniy Mukhin
<arseniy.mukhin.dev@gmail.com> wrote:

Hi,

While reviewing gist amcheck patch [1] I realized that brin amcheck
also must check if current snapshot is OK with index indcheckxmin (as
btree, gist do it). Currently this check is contained in btree amcheck
code, but other AMs need it for heapallindexed as well, so I moved it
from btree to verify_common (0003 patch).

There was a compiler warning on CI, so there is a new version with the
fix (adds #include to verify_common).
I also moved it to PG19-2.

Sorry for the noise, forgot to attach the files.

Best regards,
Arseniy Mukhin

Attachments:

v9-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v9-0001-brin-refactoring.patchDownload
From 714ee0a8f517157c5f59ff82aa1fc82e6be36921 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v9 1/4] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern and renames it to 'brin_tuple_tupdesc'.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 10 +++++-----
 src/backend/access/common/reloptions.c |  3 ++-
 src/include/access/brin.h              |  1 +
 src/include/access/brin_tuple.h        |  2 ++
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..fc67a708dda 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,8 +57,8 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
-brtuple_disk_tupdesc(BrinDesc *brdesc)
+TupleDesc
+brin_tuple_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
 	if (brdesc->bd_disktdesc == NULL)
@@ -280,7 +280,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 
 	len = hoff = MAXALIGN(len);
 
-	data_len = heap_compute_data_size(brtuple_disk_tupdesc(brdesc),
+	data_len = heap_compute_data_size(brin_tuple_tupdesc(brdesc),
 									  values, nulls);
 	len += data_len;
 
@@ -299,7 +299,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 	 * need to pass a valid null bitmap so that it will correctly skip
 	 * outputting null attributes in the data area.
 	 */
-	heap_fill_tuple(brtuple_disk_tupdesc(brdesc),
+	heap_fill_tuple(brin_tuple_tupdesc(brdesc),
 					values,
 					nulls,
 					(char *) rettuple + hoff,
@@ -682,7 +682,7 @@ brin_deconstruct_tuple(BrinDesc *brdesc,
 	 * may reuse attribute entries for more than one column, we cannot cache
 	 * offsets here.
 	 */
-	diskdsc = brtuple_disk_tupdesc(brdesc);
+	diskdsc = brin_tuple_tupdesc(brdesc);
 	stored = 0;
 	off = 0;
 	for (attnum = 0; attnum < brdesc->bd_tupdesc->natts; attnum++)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 50747c16396..bc494847341 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..2a12ab03c43 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brin_tuple_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v9-0004-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v9-0004-amcheck-brin_index_check-heap-all-indexed.patchDownload
From c45bf31d81c8d3b3495e987e7cac3fb62dffac52 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Fri, 1 Aug 2025 23:02:33 +0300
Subject: [PATCH v9 4/4] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
heap_all_indexed check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  22 +-
 contrib/amcheck/sql/check_brin.sql      |  22 +-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 490 +++++++++++++++++++++++-
 5 files changed, 565 insertions(+), 26 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 0354451c472..6337e065bb1 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regularpagescheck boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false,
+                                 consistent_operator_names text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index e5fc52ed747..be85c32bc58 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -19,7 +19,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -28,7 +28,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -88,7 +88,7 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -97,7 +97,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
@@ -122,7 +122,7 @@ SELECT brin_index_check('brintest_idx'::REGCLASS, true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
  brin_index_check 
 ------------------
  
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index b36af37fe03..4f16f31c7f8 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
 CREATE INDEX brintest_idx ON brintest USING BRIN (a);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -17,12 +17,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -34,12 +34,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -51,12 +51,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -68,12 +68,12 @@ CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -87,12 +87,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
-SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index 2c62b76cc70..51bfed7e273 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -200,6 +200,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -241,7 +290,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index d4024e76b56..b7bf1513734 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -39,6 +39,8 @@ typedef struct BrinCheckState
 	/* Check arguments */
 
 	bool		regularpagescheck;
+	bool		heapallindexed;
+	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,30 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	String	  **operatorNames;
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +113,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void check_and_prepare_operator_names(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +138,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -103,6 +147,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
 	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
+	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +173,27 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/*
+	 * We know how many attributes index has, so let's process operator names
+	 * array
+	 */
+	if (state->heapallindexed)
+	{
+		check_and_prepare_operator_names(state);
+
+		/*
+		 * Check if we are OK with indcheckxmin, and unregister snapshot as we
+		 * don't need it further
+		 */
+		UnregisterSnapshot(RegisterSnapshotAndCheckIndexCheckXMin(state->idxrel));
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -628,7 +692,6 @@ check_regular_pages(BrinCheckState * state)
 	state->regpageoffset = InvalidOffsetNumber;
 	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
 
-
 	/*
 	 * Prepare stream data for regular pages walk. It is safe to use batchmode
 	 * as block_range_read_stream_cb takes no locks.
@@ -788,6 +851,415 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_key')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "non-null" and "is_null" scan keys and consistent fn for each
+	 * attribute
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno);
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet. Also, we want to support CIC indexes.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Check operator names array input parameter and convert it to array of strings
+ * Empty input array means we use "=" operator for every attribute
+ */
+static void
+check_and_prepare_operator_names(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	state->operatorNames = palloc(sizeof(String) * state->natts);
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+	/* If we have some input check it and convert to String** */
+	if (num_elems != 0)
+	{
+		if (num_elems != state->natts)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Operator names array length %u, but index has %u attributes",
+							num_elems, state->natts)));
+		}
+
+		for (int i = 0; i < num_elems; i++)
+		{
+			if (elem_nulls[i])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator names array contains NULL")));
+			}
+			state->operatorNames[i] = makeString(TextDatumGetCString(values[i]));
+		}
+	}
+	else
+	{
+		/* If there is no input just use "=" operator for all attributes */
+		for (int i = 0; i < state->natts; i++)
+		{
+			state->operatorNames[i] = makeString("=");
+		}
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ *
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+	String	   *opname = state->operatorNames[attno - 1];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type %u",
+						opname->sval, type)));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			heap_all_indexed_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					heap_all_indexed_ereport(state, tid,
+											 "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				heap_all_indexed_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use nonnull scan key */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			heap_all_indexed_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -853,3 +1325,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
-- 
2.43.0

v9-0003-amcheck-common_verify-register-snapshot-with-indc.patchtext/x-patch; charset=US-ASCII; name=v9-0003-amcheck-common_verify-register-snapshot-with-indc.patchDownload
From c7f880d7b0c5a196b0336e5077d86644a3e26df5 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Jul 2025 17:37:13 +0300
Subject: [PATCH v9 3/4] amcheck: common_verify - register snapshot with
 indcheckxmin check

Moves check to common_verify. Every index needs it for heapallindexed check.
---
 contrib/amcheck/verify_common.c | 29 +++++++++++++++++++++++++++++
 contrib/amcheck/verify_common.h |  3 +++
 contrib/amcheck/verify_nbtree.c | 23 +----------------------
 3 files changed, 33 insertions(+), 22 deletions(-)

diff --git a/contrib/amcheck/verify_common.c b/contrib/amcheck/verify_common.c
index a31ce06ed99..bbcefe82898 100644
--- a/contrib/amcheck/verify_common.c
+++ b/contrib/amcheck/verify_common.c
@@ -189,3 +189,32 @@ index_checkable(Relation rel, Oid am_id)
 
 	return amcheck_index_mainfork_expected(rel);
 }
+
+/*
+ * 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.
+ */
+Snapshot
+RegisterSnapshotAndCheckIndexCheckXMin(Relation idxrel)
+{
+	Snapshot	snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
+	if (IsolationUsesXactSnapshot() && idxrel->rd_index->indcheckxmin &&
+		!TransactionIdPrecedes(HeapTupleHeaderGetXmin(idxrel->rd_indextuple->t_data),
+							   snapshot->xmin))
+		ereport(ERROR,
+				(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+				 errmsg("index \"%s\" cannot be verified using transaction snapshot",
+						RelationGetRelationName(idxrel))));
+
+	return snapshot;
+}
diff --git a/contrib/amcheck/verify_common.h b/contrib/amcheck/verify_common.h
index 3f4c57f963d..7b70e6a80fe 100644
--- a/contrib/amcheck/verify_common.h
+++ b/contrib/amcheck/verify_common.h
@@ -14,6 +14,7 @@
 #include "storage/lmgr.h"
 #include "storage/lockdefs.h"
 #include "utils/relcache.h"
+#include "utils/snapshot.h"
 #include "miscadmin.h"
 
 /* Typedef for callback function for amcheck_lock_relation_and_check */
@@ -26,3 +27,5 @@ extern void amcheck_lock_relation_and_check(Oid indrelid,
 											Oid am_id,
 											IndexDoCheckCallback check,
 											LOCKMODE lockmode, void *state);
+
+extern Snapshot RegisterSnapshotAndCheckIndexCheckXMin(Relation idxrel);
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 0949c88983a..c9aa9044045 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -441,28 +441,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		if (!state->readonly)
 		{
-			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),
-									   snapshot->xmin))
-				ereport(ERROR,
-						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
-						 errmsg("index \"%s\" cannot be verified using transaction snapshot",
-								RelationGetRelationName(rel))));
+			snapshot = RegisterSnapshotAndCheckIndexCheckXMin(state->rel);
 		}
 	}
 
-- 
2.43.0

v9-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v9-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From f4b9597f975e55575b332821129dd283d698309e Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v9 2/4] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 101 +++
 contrib/amcheck/t/007_verify_brin.pl    | 291 ++++++++
 contrib/amcheck/verify_brin.c           | 855 ++++++++++++++++++++++++
 8 files changed, 1407 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..0354451c472
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regularpagescheck boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..e5fc52ed747
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..b36af37fe03
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,101 @@
+-- helper func
+CREATE OR REPLACE FUNCTION  random_string( INT ) RETURNS TEXT AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::INTEGER, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+-- multiple attributes test
+CREATE TABLE brintest (id BIGSERIAL, a TEXT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops, id int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a TEXT_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_minmax_multi_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a BIGINT) WITH (FILLFACTOR = 10);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a int8_bloom_ops) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id SERIAL PRIMARY KEY, a BOX);
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+INSERT INTO brintest (a)
+SELECT BOX(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING BRIN (a BOX_INCLUSION_OPS) WITH (PAGES_PER_RANGE = 2);
+SELECT brin_index_check('brintest_idx'::REGCLASS, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..2c62b76cc70
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,291 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..d4024e76b56
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,855 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regularpagescheck;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * Walk revmap page by page from the beginning and check every revmap item.
+ * Also check that all pages within [1, lastRevmapPage] are revmap pages.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brin_tuple_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * Check all pages within the range [lastRevmapPage + 1, indexnblocks] are regular pages or new
+ * and there is a pointer in revmap to each NORMAL index tuple.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
-- 
2.43.0

#21Álvaro Herrera
alvherre@kurilemu.de
In reply to: Tomas Vondra (#16)
Re: amcheck support for BRIN indexes

On 2025-Jul-08, Tomas Vondra wrote:

On 7/8/25 14:40, Arseniy Mukhin wrote:

Thank you for the feedback! I agree with the benefits. Speaking of
(с), it seems most of the time to be really trivial to build such a
ScanKey, but not every opclass supports '=' operator. amcheck should
handle these cases somehow then. I see two options here. The first is
to not provide 'heap all indexed' check for such opclasses, which is
sad because even one core opclass (box_inclusion_ops) doesn't support
'=' operator, postgis brin opclasses don't support it too AFAICS. The
second option is to let the user define which operator to use during
the check, which, I think, makes user experience much worse in this
case. So both options look not good from the user POV as for me, so I
don't know. What do you think about it?

And should I revert the patchset to the consistent function version then?

Yeah, that's a good point. The various opclasses may support different
operators, and we don't know which "strategy" to fill into the scan key.
Minmax needs BTEqualStrategyNumber, inclusion RTContainsStrategyNumber,
and so on.

Hmm, maybe we can make the operator argument to the function an optional
argument. Then, if it's not given, use equality for the cases where
that works; if equality doesn't work for the column in that opclass,
throw an error to request an operator. That way we support the most
common case in the easy way, and for the other cases the user has to
work a little harder -- but I think it's not too bad.

I think you should have tests with indexes on more than one column.

This syntax looks terrible
SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');

the thingy at the end looks like '90s modem line noise. Can we maybe
use a variadic argument, so that if you have multiple indexed columns
you specify the operators in separate args somehow and avoid the quoting
and array decoration? I imagine something like

SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '@>', '=');
or whatever. (To think about: if I want '=' to be omitted, but I have
the second column using a type that doesn't support the '=', what's a
good syntax to use?)

Regarding 0003: I think the new function should be just
CheckIndexCheckXMin(Relation idxrel, Snapshot snap)
and make the caller responsible for the snapshot handling. Otherwise
you end up in the weird situation in 0004 where you have to do
UnregisterSnapshot(RegisterSnapshotAndDoStuff())
instead of the more ordinary
RegisterSnapshot()
CheckIndexCheckXMin()
UnregisterSnapshot()

You need an amcheck.sgml update for the new function.

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/

#22Arseniy Mukhin
arseniy.mukhin.dev@gmail.com
In reply to: Álvaro Herrera (#21)
4 attachment(s)
Re: amcheck support for BRIN indexes

Hi,

Thank you for looking into it!

On Tue, Aug 5, 2025 at 2:21 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-08, Tomas Vondra wrote:

On 7/8/25 14:40, Arseniy Mukhin wrote:

Thank you for the feedback! I agree with the benefits. Speaking of
(с), it seems most of the time to be really trivial to build such a
ScanKey, but not every opclass supports '=' operator. amcheck should
handle these cases somehow then. I see two options here. The first is
to not provide 'heap all indexed' check for such opclasses, which is
sad because even one core opclass (box_inclusion_ops) doesn't support
'=' operator, postgis brin opclasses don't support it too AFAICS. The
second option is to let the user define which operator to use during
the check, which, I think, makes user experience much worse in this
case. So both options look not good from the user POV as for me, so I
don't know. What do you think about it?

And should I revert the patchset to the consistent function version then?

Yeah, that's a good point. The various opclasses may support different
operators, and we don't know which "strategy" to fill into the scan key.
Minmax needs BTEqualStrategyNumber, inclusion RTContainsStrategyNumber,
and so on.

Hmm, maybe we can make the operator argument to the function an optional
argument. Then, if it's not given, use equality for the cases where
that works; if equality doesn't work for the column in that opclass,
throw an error to request an operator. That way we support the most
common case in the easy way, and for the other cases the user has to
work a little harder -- but I think it's not too bad.

Yes, the operator list is an optional argument now. Like you said, if
it's not passed to the function call, the equality operator is used.

I realized that solving the problem with opclasses without equality
operator by letting user to define operator list has several
drawbacks:

It's not very convenient to call automatically? Because the calls are
different from index to index. You can't just call
brin_index_check('index', true, true) on everything. Maybe I'm wrong,
but it seems like amcheck is a tool that is often used to periodically
check the health of Postgres clusters (and there can be many of them),
so users probably don't want to get into the details of each index.

Also, it seems like we don't want the user to define the operator to
check. We want them to pass in the "correct" operator if there is no
equality operator. So there's no choice, we just want users to figure
out what the correct operator is and pass it in. But we already know
what the "correct" operator is. Maybe we should just implement an
opclass <-> "correct" operator mapping on the database side? We also
need opclass developers to be able to add such a mapping if they want
their opclass to be supported by amcheck. Then during the check we can
look up into the mapping and use the operators. I was thinking about a
new catalog table or maybe adding it to BrinOpcInfo? Probably there is
a better way to do it? If the mapping doesn't have an operator for
opclass - no problem, we can skip the consistentFn call for such
columns and maybe log a message about it. This way we don't have all
these problems with operator list argument and with false positives
when a user fails to realize what the "correct" operator is.

I think you should have tests with indexes on more than one column.

There is one multicolumn index test, I added another one where the
list of operators is passed. Also added tests with invalid operator
list.

This syntax looks terrible
SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '{"@>"}');

the thingy at the end looks like '90s modem line noise. Can we maybe
use a variadic argument, so that if you have multiple indexed columns
you specify the operators in separate args somehow and avoid the quoting
and array decoration? I imagine something like

SELECT brin_index_check('brintest_idx'::REGCLASS, true, true, '@>', '=');
or whatever. (To think about: if I want '=' to be omitted, but I have
the second column using a type that doesn't support the '=', what's a
good syntax to use?)

Good idea! It looks better. I changed the operator list to variadic
argument. For now brin_index_check() expects the user to define all or
nothing. I think if we want to allow the user to omit operators for
some columns, then we need to know the indexes of the columns for
which the passed operators are intended. Something like this maybe:

brin_index_check('brintest_idx', true, true, 1, '@>', 3, '@>');

Regarding 0003: I think the new function should be just
CheckIndexCheckXMin(Relation idxrel, Snapshot snap)
and make the caller responsible for the snapshot handling. Otherwise
you end up in the weird situation in 0004 where you have to do
UnregisterSnapshot(RegisterSnapshotAndDoStuff())
instead of the more ordinary
RegisterSnapshot()
CheckIndexCheckXMin()
UnregisterSnapshot()

Done. BTW gist amcheck also needs 0003 [0]/messages/by-id/5FC1B5B6-FB35-44A2-AB62-632F14E958C5@yandex-team.ru. Maybe we can move it to
the separate thread and commit, so both patches can use it? What do
you think, Andrey?

You need an amcheck.sgml update for the new function.

Done. The operator list is the most controversial part. It was not
easy to figure out how to describe the criteria of the "correct"
operator, I hope it's not very confusing.

And I have a question, is it somehow helpful that 'index structure
check' and 'heap all indexed' are in the different patches? It makes
it a bit more difficult to update the patchset, so if it's not useful
for reviewers I would probably merge it in the next version.

So here is a new version.

Thank you!

[0]: /messages/by-id/5FC1B5B6-FB35-44A2-AB62-632F14E958C5@yandex-team.ru

Best regards,
Arseniy Mukhin

Attachments:

v10-0001-brin-refactoring.patchtext/x-patch; charset=US-ASCII; name=v10-0001-brin-refactoring.patchDownload
From 756ce48a8a467f03183cc34630229262436a2880 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Wed, 16 Apr 2025 11:26:45 +0300
Subject: [PATCH v10 1/4] brin refactoring

For adding BRIN index support in amcheck we need some tiny changes in BRIN
core code:

* We need to have tuple descriptor for on-disk storage of BRIN tuples.
  It is a public field 'bd_disktdesc' in BrinDesc, but to access it we
  need function 'brtuple_disk_tupdesc' which is internal. This commit
  makes it extern and renames it to 'brin_tuple_tupdesc'.

* For meta page check we need to know pages_per_range upper limit. It's
  hardcoded now. This commit moves its value to macros BRIN_MAX_PAGES_PER_RANGE
  so that we can use it in amcheck too.
---
 src/backend/access/brin/brin_tuple.c   | 10 +++++-----
 src/backend/access/common/reloptions.c |  3 ++-
 src/include/access/brin.h              |  1 +
 src/include/access/brin_tuple.h        |  2 ++
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..fc67a708dda 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -57,8 +57,8 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc,
 /*
  * Return a tuple descriptor used for on-disk storage of BRIN tuples.
  */
-static TupleDesc
-brtuple_disk_tupdesc(BrinDesc *brdesc)
+TupleDesc
+brin_tuple_tupdesc(BrinDesc *brdesc)
 {
 	/* We cache these in the BrinDesc */
 	if (brdesc->bd_disktdesc == NULL)
@@ -280,7 +280,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 
 	len = hoff = MAXALIGN(len);
 
-	data_len = heap_compute_data_size(brtuple_disk_tupdesc(brdesc),
+	data_len = heap_compute_data_size(brin_tuple_tupdesc(brdesc),
 									  values, nulls);
 	len += data_len;
 
@@ -299,7 +299,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 	 * need to pass a valid null bitmap so that it will correctly skip
 	 * outputting null attributes in the data area.
 	 */
-	heap_fill_tuple(brtuple_disk_tupdesc(brdesc),
+	heap_fill_tuple(brin_tuple_tupdesc(brdesc),
 					values,
 					nulls,
 					(char *) rettuple + hoff,
@@ -682,7 +682,7 @@ brin_deconstruct_tuple(BrinDesc *brdesc,
 	 * may reuse attribute entries for more than one column, we cannot cache
 	 * offsets here.
 	 */
-	diskdsc = brtuple_disk_tupdesc(brdesc);
+	diskdsc = brin_tuple_tupdesc(brdesc);
 	stored = 0;
 	off = 0;
 	for (attnum = 0; attnum < brdesc->bd_tupdesc->natts; attnum++)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0af3fea68fa..27f2b6f5e05 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -22,6 +22,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/nbtree.h"
+#include "access/brin.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
 #include "catalog/pg_type.h"
@@ -343,7 +344,7 @@ static relopt_int intRelOpts[] =
 			"Number of pages that each page range covers in a BRIN index",
 			RELOPT_KIND_BRIN,
 			AccessExclusiveLock
-		}, 128, 1, 131072
+		}, 128, 1, BRIN_MAX_PAGES_PER_RANGE
 	},
 	{
 		{
diff --git a/src/include/access/brin.h b/src/include/access/brin.h
index 821f1e02806..334ce973b67 100644
--- a/src/include/access/brin.h
+++ b/src/include/access/brin.h
@@ -37,6 +37,7 @@ typedef struct BrinStatsData
 
 
 #define BRIN_DEFAULT_PAGES_PER_RANGE	128
+#define BRIN_MAX_PAGES_PER_RANGE	131072
 #define BrinGetPagesPerRange(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == BRIN_AM_OID), \
diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h
index 010ba4ea3c0..2a12ab03c43 100644
--- a/src/include/access/brin_tuple.h
+++ b/src/include/access/brin_tuple.h
@@ -109,4 +109,6 @@ extern BrinMemTuple *brin_memtuple_initialize(BrinMemTuple *dtuple,
 extern BrinMemTuple *brin_deform_tuple(BrinDesc *brdesc,
 									   BrinTuple *tuple, BrinMemTuple *dMemtuple);
 
+extern TupleDesc brin_tuple_tupdesc(BrinDesc *brdesc);
+
 #endif							/* BRIN_TUPLE_H */
-- 
2.43.0

v10-0004-amcheck-brin_index_check-heap-all-indexed.patchtext/x-patch; charset=US-ASCII; name=v10-0004-amcheck-brin_index_check-heap-all-indexed.patchDownload
From 531aeac7310864ac4f59573bd71254e763adab70 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Sun, 10 Aug 2025 16:25:08 +0300
Subject: [PATCH v10 4/4] amcheck: brin_index_check() - heap all indexed

This commit extends functionality of brin_index_check() with
heap_all_indexed check: we validate every index range tuple
against every heap tuple within the range using consistentFn.
Also, we check here that fields 'has_nulls', 'all_nulls' and
'empty_range' are consistent with the range heap data. It's the most
expensive part of the brin_index_check(), so it's optional.
---
 contrib/amcheck/amcheck--1.5--1.6.sql   |   6 +-
 contrib/amcheck/expected/check_brin.out |  60 ++-
 contrib/amcheck/sql/check_brin.sql      |  54 ++-
 contrib/amcheck/t/007_verify_brin.pl    |  51 ++-
 contrib/amcheck/verify_brin.c           | 501 ++++++++++++++++++++++++
 doc/src/sgml/amcheck.sgml               |  39 +-
 6 files changed, 685 insertions(+), 26 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql
index 0354451c472..55276527e68 100644
--- a/contrib/amcheck/amcheck--1.5--1.6.sql
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -8,11 +8,13 @@
 -- brin_index_check()
 --
 CREATE FUNCTION brin_index_check(index regclass,
-                                 regularpagescheck boolean default false
+                                 regularpagescheck boolean default false,
+                                 heapallindexed boolean default false,
+                                 variadic text[] default '{}'
 )
     RETURNS VOID
 AS 'MODULE_PATHNAME', 'brin_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
 
 -- We don't want this to be available to public
-REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean, boolean, text[]) FROM PUBLIC;
\ No newline at end of file
diff --git a/contrib/amcheck/expected/check_brin.out b/contrib/amcheck/expected/check_brin.out
index 6890fff46bd..909b41cb7a9 100644
--- a/contrib/amcheck/expected/check_brin.out
+++ b/contrib/amcheck/expected/check_brin.out
@@ -5,7 +5,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
 CREATE INDEX brintest_idx ON brintest USING brin (a);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -19,7 +19,7 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -28,7 +28,7 @@ SELECT brin_index_check('brintest_idx', true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -42,7 +42,7 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -51,7 +51,7 @@ SELECT brin_index_check('brintest_idx', true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -65,7 +65,7 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -74,7 +74,7 @@ SELECT brin_index_check('brintest_idx', true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -90,7 +90,7 @@ SELECT box(point(random() * 1000, random() * 1000), point(random() * 1000, rando
 FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true, '@>');
  brin_index_check 
 ------------------
  
@@ -99,7 +99,7 @@ SELECT brin_index_check('brintest_idx', true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true, '@>');
  brin_index_check 
 ------------------
  
@@ -113,7 +113,7 @@ CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
@@ -122,12 +122,50 @@ SELECT brin_index_check('brintest_idx', true);
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
  brin_index_check 
 ------------------
  
 (1 row)
 
+-- cleanup
+DROP TABLE brintest;
+-- multiple attributes test with custom operators
+CREATE TABLE brintest (id bigserial, a text, b box) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops, b box_inclusion_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a, b) SELECT
+                                random_string((x % 100)),
+                                box(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@>');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops, b box_inclusion_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@>');
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- error if it's impossible to use default operator for all index attributes
+SELECT brin_index_check('brintest_idx', true, true);
+ERROR:  Operator = is not a member of operator family "box_inclusion_ops"
+-- error if number of operators in input doesn't match index attributes number
+SELECT brin_index_check('brintest_idx', true, true, '=');
+ERROR:  Number of operator names in input (1) doesn't match index attributes number (3)
+-- error if operator name is NULL
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', NULL);
+ERROR:  Operator name must not be NULL
+-- error if there is no operator for attribute type
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@@');
+ERROR:  There is no operator @@ for type "box"
 -- cleanup
 DROP TABLE brintest;
 -- cleanup
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
index 1c97b370cac..66dd1647d3b 100644
--- a/contrib/amcheck/sql/check_brin.sql
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -7,7 +7,7 @@ $$ LANGUAGE sql;
 -- empty table index should be valid
 CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
 CREATE INDEX brintest_idx ON brintest USING brin (a);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -17,12 +17,12 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -34,12 +34,12 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -51,12 +51,12 @@ CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_
 INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE a > 20000 AND a < 40000;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 -- cleanup
 DROP TABLE brintest;
 
@@ -70,12 +70,12 @@ FROM generate_series(1, 10000);
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 2000 AND id < 4000;
 
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true, '@>');
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true, '@>');
 -- cleanup
 DROP TABLE brintest;
 
@@ -86,12 +86,44 @@ CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_min
 INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
 -- create some empty ranges
 DELETE FROM brintest WHERE id > 1500 AND id < 2500;
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
 
 -- rebuild index
 DROP INDEX brintest_idx;
 CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
-SELECT brin_index_check('brintest_idx', true);
+SELECT brin_index_check('brintest_idx', true, true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multiple attributes test with custom operators
+CREATE TABLE brintest (id bigserial, a text, b box) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops, b box_inclusion_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a, b) SELECT
+                                random_string((x % 100)),
+                                box(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@>');
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops, b box_inclusion_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@>');
+
+-- error if it's impossible to use default operator for all index attributes
+SELECT brin_index_check('brintest_idx', true, true);
+
+-- error if number of operators in input doesn't match index attributes number
+SELECT brin_index_check('brintest_idx', true, true, '=');
+
+-- error if operator name is NULL
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', NULL);
+
+-- error if there is no operator for attribute type
+SELECT brin_index_check('brintest_idx', true, true, '=', '=', '@@');
+
 -- cleanup
 DROP TABLE brintest;
 
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
index c4073f9bdcc..6fe0c78f12e 100644
--- a/contrib/amcheck/t/007_verify_brin.pl
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -210,6 +210,55 @@ my @tests = (
             return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
         },
         expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    },
+    {
+        # range is marked as empty_range, but heap has some data for the range
+
+        find     => pack('LCC', 0, 0x88, 0x03),
+        replace  => pack('LCC', 0, 0xA8, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null););
+        },
+        expected   => wrap('range is marked as empty but contains qualified live tuples. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range hasnulls & allnulls are false, but heap contains null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x00),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range hasnulls and allnulls are false, but contains a null value. Range blkno: 0, heap tid (0,1)')
+    },
+    {
+        # range allnulls is true, but heap contains non-null values for the range
+
+        find     => pack('LCC', 0, 0x88, 0x02),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES (null), ('aaaaa'););
+        },
+        expected   => wrap('range allnulls is true, but contains nonnull value. Range blkno: 0, heap tid (0,2)')
+    },
+    {
+        # consistent function return FALSE for the valid heap value
+        # replace "ccccc" with "bbbbb" so that min_max index was too narrow
+
+        find       => 'ccccc',
+        replace    => 'bbbbb',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'), ('ccccc'););
+        },
+        expected   => wrap('heap tuple inconsistent with index. Range blkno: 0, heap tid (0,2)')
     }
 );
 
@@ -251,7 +300,7 @@ foreach my $test_struct (@tests) {
 $node->start;
 
 foreach my $test_struct (@tests) {
-    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true, true)));
     like($stderr, $test_struct->{expected});
 }
 
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
index 183d189685c..aae7ec3c0d7 100644
--- a/contrib/amcheck/verify_brin.c
+++ b/contrib/amcheck/verify_brin.c
@@ -39,6 +39,8 @@ typedef struct BrinCheckState
 	/* Check arguments */
 
 	bool		regularpagescheck;
+	bool		heapallindexed;
+	ArrayType  *consistent_oper_names;
 
 	/* BRIN check common fields */
 
@@ -67,6 +69,29 @@ typedef struct BrinCheckState
 	Page		regpage;
 	OffsetNumber regpageoffset;
 
+	/* Heap all indexed check fields */
+
+	BrinRevmap *revmap;
+	Buffer		buf;
+	FmgrInfo   *consistentFn;
+	/* Scan keys for regular values */
+	ScanKey    *nonnull_sk;
+	/* Scan keys for null values */
+	ScanKey    *isnull_sk;
+	double		range_cnt;
+	/* first block of the next range */
+	BlockNumber nextrangeBlk;
+
+	/*
+	 * checkable_range shows if current range could be checked and dtup
+	 * contains valid index tuple for the range. It could be false if the
+	 * current range is not summarized, or it's placeholder, or it's just a
+	 * beginning of the check
+	 */
+	bool		checkable_range;
+	BrinMemTuple *dtup;
+	MemoryContext rangeCtx;
+	MemoryContext heaptupleCtx;
 }			BrinCheckState;
 
 static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
@@ -87,6 +112,23 @@ static bool revmap_points_to_index_tuple(BrinCheckState * state);
 
 static ItemId PageGetItemIdCareful(BrinCheckState * state);
 
+static void check_heap_all_indexed(BrinCheckState * state);
+
+static void prepare_nonnull_scan_keys(BrinCheckState * state);
+
+static void brin_check_callback(Relation index,
+								ItemPointer tid,
+								Datum *values,
+								bool *isnull,
+								bool tupleIsAlive,
+								void *brstate);
+
+static void check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid);
+
+static ScanKey prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno, String *opname);
+
+static ScanKey prepare_isnull_scan_key(AttrNumber attno);
+
 static void brin_check_ereport(BrinCheckState * state, const char *fmt);
 
 static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
@@ -95,6 +137,7 @@ static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
 
 static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
 
+static void heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message);
 
 Datum
 brin_index_check(PG_FUNCTION_ARGS)
@@ -103,6 +146,8 @@ brin_index_check(PG_FUNCTION_ARGS)
 	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
 
 	state->regularpagescheck = PG_GETARG_BOOL(1);
+	state->heapallindexed = PG_GETARG_BOOL(2);
+	state->consistent_oper_names = PG_GETARG_ARRAYTYPE_P(3);
 
 	amcheck_lock_relation_and_check(indrelid,
 									BRIN_AM_OID,
@@ -127,9 +172,31 @@ brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonl
 	state->bdesc = brin_build_desc(idxrel);
 	state->natts = state->bdesc->bd_tupdesc->natts;
 
+	/* Do some preparations and checks for heapallindexed */
+	if (state->heapallindexed)
+	{
+		/*
+		 * Check if we are OK with indcheckxmin, and unregister snapshot as we
+		 * don't need it further
+		 */
+		Snapshot	snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
+		check_indcheckxmin(state->idxrel, snapshot);
+		UnregisterSnapshot(snapshot);
+
+		/*
+		 * If there are some problems with scan keys generation or operator
+		 * name array is invalid we want to fail fast. So do it here.
+		 */
+		prepare_nonnull_scan_keys(state);
+	}
 
 	check_brin_index_structure(state);
 
+	if (state->heapallindexed)
+	{
+		check_heap_all_indexed(state);
+	}
 
 	brin_free_desc(state->bdesc);
 }
@@ -797,6 +864,424 @@ PageGetItemIdCareful(BrinCheckState * state)
 	return itemid;
 }
 
+/*
+ * Check that every heap tuple are consistent with the index.
+ *
+ * Here we generate ScanKey for every heap tuple and test it against
+ * appropriate range using consistentFn (for ScanKey generation logic look 'prepare_nonnull_scan_keys')
+ *
+ * Also, we check that fields 'empty_range', 'all_nulls' and 'has_nulls'
+ * are not too "narrow" for each range, which means:
+ * 1) has_nulls = false, but we see null value (only for oi_regular_nulls is true)
+ * 2) all_nulls = true, but we see nonnull value.
+ * 3) empty_range = true, but we see tuple within the range.
+ *
+ * We use allowSync = false, because this way
+ * we process full ranges one by one from the first range.
+ * It's not necessary, but makes the code simpler and this way
+ * we need to fetch every index tuple only once.
+ */
+static void
+check_heap_all_indexed(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	Relation	heaprel = state->heaprel;
+	double		reltuples;
+	IndexInfo  *indexInfo;
+
+	/* heap all indexed check fields initialization */
+
+	state->revmap = brinRevmapInitialize(idxrel, &state->pagesPerRange);
+	state->dtup = brin_new_memtuple(state->bdesc);
+	state->checkable_range = false;
+	state->consistentFn = palloc0_array(FmgrInfo, state->natts);
+	state->range_cnt = 0;
+	/* next range is the first range in the beginning */
+	state->nextrangeBlk = 0;
+	state->isnull_sk = palloc0_array(ScanKey, state->natts);
+	state->rangeCtx = AllocSetContextCreate(CurrentMemoryContext,
+											"brin check range context",
+											ALLOCSET_DEFAULT_SIZES);
+	state->heaptupleCtx = AllocSetContextCreate(CurrentMemoryContext,
+												"brin check tuple context",
+												ALLOCSET_DEFAULT_SIZES);
+
+	/*
+	 * Prepare "is_null" scan keys and consistent fn for each attribute.
+	 * "non-null" scan keys are already generated.
+	 */
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		FmgrInfo   *tmp;
+
+		tmp = index_getprocinfo(idxrel, attno, BRIN_PROCNUM_CONSISTENT);
+		fmgr_info_copy(&state->consistentFn[attno - 1], tmp, CurrentMemoryContext);
+
+		state->isnull_sk[attno - 1] = prepare_isnull_scan_key(attno);
+	}
+
+	indexInfo = BuildIndexInfo(idxrel);
+
+	/*
+	 * Use snapshot to check only those tuples that are guaranteed to be
+	 * indexed already. Using SnapshotAny would make it more difficult to say
+	 * if there is a corruption or checked tuple just haven't been indexed
+	 * yet. Also, we want to support CIC indexes.
+	 */
+	indexInfo->ii_Concurrent = true;
+	reltuples = table_index_build_scan(heaprel, idxrel, indexInfo, false, true,
+									   brin_check_callback, (void *) state, NULL);
+
+	elog(DEBUG3, "ranges were checked: %f", state->range_cnt);
+	elog(DEBUG3, "scan total tuples: %f", reltuples);
+
+	if (state->buf != InvalidBuffer)
+		ReleaseBuffer(state->buf);
+
+	brinRevmapTerminate(state->revmap);
+	MemoryContextDelete(state->rangeCtx);
+	MemoryContextDelete(state->heaptupleCtx);
+}
+
+/*
+ * Generate scan keys for every index attribute.
+ *
+ * ConsistentFn requires ScanKey, so we need to generate ScanKey for every
+ * attribute somehow. We want ScanKey that would result in TRUE for every heap
+ * tuple within the range when we use its indexed value as sk_argument.
+ * To generate such a ScanKey we need to define the right operand type and the strategy number.
+ * Right operand type is a type of data that index is built on, so it's 'opcintype'.
+ * There is no strategy number that we can always use,
+ * because every opclass defines its own set of operators it supports and strategy number
+ * for the same operator can differ from opclass to opclass.
+ * So to get strategy number we look up an operator that gives us desired behavior
+ * and which both operand types are 'opcintype' and then retrieve the strategy number for it.
+ * Most of the time we can use '='. We let user define operator name in case opclass doesn't
+ * support '=' operator. Also, if such operator doesn't exist, we can't proceed with the check.
+ *
+ * If operator name array is empty use "=" operator for every attribute.
+ */
+static void
+prepare_nonnull_scan_keys(BrinCheckState * state)
+{
+	Oid			element_type = ARR_ELEMTYPE(state->consistent_oper_names);
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Datum	   *values;
+	bool	   *elem_nulls;
+	int			num_elems;
+
+	get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
+	deconstruct_array(state->consistent_oper_names, element_type, typlen, typbyval, typalign,
+					  &values, &elem_nulls, &num_elems);
+
+
+	/*
+	 * If we have some input, check that number of operators in the input is
+	 * relevant to the index
+	 */
+	if (num_elems > 0 && num_elems != state->natts)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Number of operator names in input (%u) "
+						"doesn't match index attributes number (%u)",
+						num_elems, state->natts)));
+	}
+
+
+	/* Generate scan key for every index attribute */
+	state->nonnull_sk = palloc0_array(ScanKey, state->natts);
+
+	for (AttrNumber attno = 1; attno <= state->natts; attno++)
+	{
+		String	   *operatorName;
+
+		if (num_elems > 0)
+		{
+
+			if (elem_nulls[attno - 1])
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Operator name must not be NULL")));
+			}
+
+			operatorName = makeString(TextDatumGetCString(values[attno - 1]));
+		}
+		else
+		{
+
+			/* Use '=' as default operator */
+			operatorName = makeString("=");
+		}
+
+		state->nonnull_sk[attno - 1] = prepare_nonnull_scan_key(state, attno, operatorName);
+		pfree(operatorName);
+	}
+}
+
+/*
+ * Prepare ScanKey for index attribute.
+ *
+ * Generated once, and will be reused for all heap tuples.
+ * Argument field will be filled for every heap tuple before
+ * consistent function invocation, so leave it NULL for a while.
+ */
+static ScanKey
+prepare_nonnull_scan_key(const BrinCheckState * state, AttrNumber attno, String *opname)
+{
+	ScanKey		scanKey;
+	Oid			opOid;
+	Oid			opFamilyOid;
+	bool		defined;
+	StrategyNumber strategy;
+	RegProcedure opRegProc;
+	List	   *operNameList;
+	int			attindex = attno - 1;
+	Form_pg_attribute attr = TupleDescAttr(state->bdesc->bd_tupdesc, attindex);
+	Oid			type = state->idxrel->rd_opcintype[attindex];
+
+	opFamilyOid = state->idxrel->rd_opfamily[attindex];
+	operNameList = list_make1(opname);
+	opOid = OperatorLookup(operNameList, type, type, &defined);
+
+	if (opOid == InvalidOid)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("There is no operator %s for type \"%s\"",
+						opname->sval, format_type_be(type))));
+	}
+
+	strategy = get_op_opfamily_strategy(opOid, opFamilyOid);
+
+	if (strategy == 0)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("Operator %s is not a member of operator family \"%s\"",
+						opname->sval,
+						get_opfamily_name(opFamilyOid, false))));
+	}
+
+	opRegProc = get_opcode(opOid);
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(
+						   scanKey,
+						   0,
+						   attno,
+						   strategy,
+						   type,
+						   attr->attcollation,
+						   opRegProc,
+						   (Datum) NULL
+		);
+	pfree(operNameList);
+
+	return scanKey;
+}
+
+static ScanKey
+prepare_isnull_scan_key(AttrNumber attno)
+{
+	ScanKey		scanKey;
+
+	scanKey = palloc0(sizeof(ScanKeyData));
+	ScanKeyEntryInitialize(scanKey,
+						   SK_ISNULL | SK_SEARCHNULL,
+						   attno,
+						   InvalidStrategy,
+						   InvalidOid,
+						   InvalidOid,
+						   InvalidOid,
+						   (Datum) 0);
+	return scanKey;
+}
+
+/*
+ * We walk from the first range (blkno = 0) to the last as the scan proceed.
+ * For every heap tuple we check if we are done with the current range, and we need to move further
+ * to the current heap tuple's range. While moving to the next range we check that it's not empty (because
+ * we have at least one tuple for this range).
+ * Every heap tuple are checked to be consistent with the range it belongs to.
+ * In case of unsummarized ranges and placeholders we skip all checks.
+ *
+ * While moving, we may jump over some ranges,
+ * but it's okay because we would not be able to check them anyway.
+ * We also can't say whether skipped ranges should be marked as empty or not,
+ * since it's possible that there were some tuples before that are now deleted.
+ *
+ */
+static void
+brin_check_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *brstate)
+{
+	BrinCheckState *state;
+	BlockNumber heapblk;
+
+	state = (BrinCheckState *) brstate;
+	heapblk = ItemPointerGetBlockNumber(tid);
+
+	/* If we went beyond the current range let's fetch new range */
+	if (heapblk >= state->nextrangeBlk)
+	{
+		BrinTuple  *tup;
+		BrinTuple  *tupcopy = NULL;
+		MemoryContext oldCtx;
+		OffsetNumber off;
+		Size		size;
+		Size		btupsz = 0;
+
+		MemoryContextReset(state->rangeCtx);
+		oldCtx = MemoryContextSwitchTo(state->rangeCtx);
+
+		state->range_cnt++;
+
+		/* Move to the range that contains current heap tuple */
+		tup = brinGetTupleForHeapBlock(state->revmap, heapblk, &state->buf,
+									   &off, &size, BUFFER_LOCK_SHARE);
+
+		if (tup)
+		{
+			tupcopy = brin_copy_tuple(tup, size, tupcopy, &btupsz);
+			LockBuffer(state->buf, BUFFER_LOCK_UNLOCK);
+			state->dtup = brin_deform_tuple(state->bdesc, tupcopy, state->dtup);
+
+			/* We can't check placeholder ranges */
+			state->checkable_range = !state->dtup->bt_placeholder;
+		}
+		else
+		{
+			/* We can't check unsummarized ranges. */
+			state->checkable_range = false;
+		}
+
+		/*
+		 * Update nextrangeBlk so we know when we are done with the current
+		 * range
+		 */
+		state->nextrangeBlk = (heapblk / state->pagesPerRange + 1) * state->pagesPerRange;
+
+		MemoryContextSwitchTo(oldCtx);
+
+		/* Range must not be empty */
+		if (state->checkable_range && state->dtup->bt_empty_range)
+		{
+			heap_all_indexed_ereport(state, tid, "range is marked as empty but contains qualified live tuples");
+		}
+
+	}
+
+	/* Check tuple is consistent with the index */
+	if (state->checkable_range)
+	{
+		check_heap_tuple(state, values, isnull, tid);
+	}
+
+}
+
+/*
+ * We check hasnulls flags for null values and oi_regular_nulls = true,
+ * check allnulls is false for all nonnull values not matter oi_regular_nulls is set or not,
+ * For all other cases we call consistentFn with appropriate scanKey:
+ * - for oi_regular_nulls = false and null values we use 'isNull' scanKey,
+ * - for nonnull values we use 'nonnull' scanKey
+ */
+static void
+check_heap_tuple(BrinCheckState * state, const Datum *values, const bool *nulls, ItemPointer tid)
+{
+	int			attindex;
+	BrinMemTuple *dtup = state->dtup;
+	BrinDesc   *bdesc = state->bdesc;
+	MemoryContext oldCtx;
+
+	Assert(state->checkable_range);
+
+	MemoryContextReset(state->heaptupleCtx);
+	oldCtx = MemoryContextSwitchTo(state->heaptupleCtx);
+
+	/* check every index attribute */
+	for (attindex = 0; attindex < state->natts; attindex++)
+	{
+		BrinValues *bval;
+		Datum		consistentFnResult;
+		bool		consistent;
+		ScanKey		scanKey;
+		bool		oi_regular_nulls = bdesc->bd_info[attindex]->oi_regular_nulls;
+
+		bval = &dtup->bt_columns[attindex];
+
+		if (nulls[attindex])
+		{
+			/*
+			 * Use hasnulls flag for oi_regular_nulls is true. Otherwise,
+			 * delegate check to consistentFn
+			 */
+			if (oi_regular_nulls)
+			{
+				/* We have null value, so hasnulls or allnulls must be true */
+				if (!(bval->bv_hasnulls || bval->bv_allnulls))
+				{
+					heap_all_indexed_ereport(state, tid,
+											 "range hasnulls and allnulls are false, but contains a null value");
+				}
+				continue;
+			}
+
+			/*
+			 * In case of null and oi_regular_nulls = false we use isNull
+			 * scanKey for invocation of consistentFn
+			 */
+			scanKey = state->isnull_sk[attindex];
+		}
+		else
+		{
+			/* We have a nonnull value, so allnulls should be false */
+			if (bval->bv_allnulls)
+			{
+				heap_all_indexed_ereport(state, tid, "range allnulls is true, but contains nonnull value");
+			}
+
+			/* use nonnull scan key */
+			scanKey = state->nonnull_sk[attindex];
+			scanKey->sk_argument = values[attindex];
+		}
+
+		/* If oi_regular_nulls = true we should never get there with null */
+		Assert(!oi_regular_nulls || !nulls[attindex]);
+
+		if (state->consistentFn[attindex].fn_nargs >= 4)
+		{
+			consistentFnResult = FunctionCall4Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(&scanKey),
+												   Int32GetDatum(1)
+				);
+		}
+		else
+		{
+			consistentFnResult = FunctionCall3Coll(&state->consistentFn[attindex],
+												   state->idxrel->rd_indcollation[attindex],
+												   PointerGetDatum(state->bdesc),
+												   PointerGetDatum(bval),
+												   PointerGetDatum(scanKey)
+				);
+		}
+
+		consistent = DatumGetBool(consistentFnResult);
+
+		if (!consistent)
+		{
+			heap_all_indexed_ereport(state, tid, "heap tuple inconsistent with index");
+		}
+
+	}
+
+	MemoryContextSwitchTo(oldCtx);
+}
 
 /* Report without any additional info */
 static void
@@ -862,3 +1347,19 @@ revmap_item_ereport(BrinCheckState * state, const char *fmt)
 					state->revmapBlk,
 					state->revmapidx)));
 }
+
+/* Report with range blkno, heap tuple info */
+static void
+heap_all_indexed_ereport(const BrinCheckState * state, const ItemPointerData *tid, const char *message)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is not consistent with the heap - %s. Range blkno: %u, heap tid (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					message,
+					state->dtup->bt_blkno,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+}
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 2f76af907ec..8ed5fe6ebfd 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -233,7 +233,7 @@ SET client_min_messages = DEBUG1;
   <variablelist>
    <varlistentry>
     <term>
-     <function>brin_index_check(index regclass, regularpagescheck boolean) returns void</function>
+     <function>brin_index_check(index regclass, regularpagescheck boolean, heapallindexed boolean, variadic text[]) returns void</function>
      <indexterm>
       <primary>brin_index_check</primary>
      </indexterm>
@@ -261,6 +261,43 @@ SET client_min_messages = DEBUG1;
         </para>
        </listitem>
       </varlistentry>
+      <varlistentry>
+       <term><literal>heapallindexed</literal></term>
+       <listitem>
+        <para>
+         If true, the check verifies that every heap tuple is consistent with the
+         index. This check phase needs an operator for which an expression
+         <literal>LHS OPERATOR RHS</literal> evaluates to <literal>true</literal>
+         when we use the same value of the indexed type for both <literal>LHS</literal> and <literal>RHS</literal>.
+         For example, if the indexed column's type is <type>bigint</type>,
+         equality operator can be used because expression
+         <literal>x = x</literal> result in <literal>true</literal>
+         for every value of <type>bigint</type>
+         (e.g. <literal>1 = 1</literal> is <literal>true</literal>, <literal>2 = 2</literal> is <literal>true</literal>, and so on).
+         Operator also should be part of the operator family of the indexed column.
+         Most of the time, the equality operator can be used.
+         If all indexed column operator classes support equality operator,
+         the function call looks like this:
+<programlisting>
+   SELECT brin_index_check('index_name', true, true);
+</programlisting>
+         If any indexed column operator class doesn't support equality operator then
+         a suitable operator for every such column should be found and
+         operators for all indexed columns should be listed in the function call.
+         For instance, we have two indexed columns
+         (<parameter>a</parameter> <type>int8_minmax_ops</type>, <parameter>b</parameter> <type>box_inclusion_ops</type>).
+         <type>box_inclusion_ops</type> operator class does not support equality operator.
+         The appropriate operator would be <literal>@></literal>.
+         Then the function call looks like this:
+<programlisting>
+   SELECT brin_index_check('index_name', true, true, '=', '@>');
+</programlisting>
+        </para>
+        <para>
+         Defaults to false.
+        </para>
+       </listitem>
+      </varlistentry>
      </variablelist>
     </listitem>
    </varlistentry>
-- 
2.43.0

v10-0003-amcheck-common_verify-snapshot-indcheckxmin-chec.patchtext/x-patch; charset=US-ASCII; name=v10-0003-amcheck-common_verify-snapshot-indcheckxmin-chec.patchDownload
From 218e39d073a50aed1fd30a010a792b38d396951b Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Tue, 22 Jul 2025 17:37:13 +0300
Subject: [PATCH v10 3/4] amcheck: common_verify - snapshot indcheckxmin check

Moves check to common_verify. Every index needs it for heapallindexed check.
---
 contrib/amcheck/verify_common.c | 25 +++++++++++++++++++++++++
 contrib/amcheck/verify_common.h |  3 +++
 contrib/amcheck/verify_nbtree.c | 22 +---------------------
 3 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/contrib/amcheck/verify_common.c b/contrib/amcheck/verify_common.c
index a31ce06ed99..c8d14ac158f 100644
--- a/contrib/amcheck/verify_common.c
+++ b/contrib/amcheck/verify_common.c
@@ -189,3 +189,28 @@ index_checkable(Relation rel, Oid am_id)
 
 	return amcheck_index_mainfork_expected(rel);
 }
+
+/*
+ * 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.
+ */
+void
+check_indcheckxmin(Relation idxrel, Snapshot snapshot)
+{
+	if (IsolationUsesXactSnapshot() && idxrel->rd_index->indcheckxmin &&
+		!TransactionIdPrecedes(HeapTupleHeaderGetXmin(idxrel->rd_indextuple->t_data),
+							   snapshot->xmin))
+		ereport(ERROR,
+				(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+				 errmsg("index \"%s\" cannot be verified using transaction snapshot",
+						RelationGetRelationName(idxrel))));
+}
diff --git a/contrib/amcheck/verify_common.h b/contrib/amcheck/verify_common.h
index 3f4c57f963d..711514707b2 100644
--- a/contrib/amcheck/verify_common.h
+++ b/contrib/amcheck/verify_common.h
@@ -14,6 +14,7 @@
 #include "storage/lmgr.h"
 #include "storage/lockdefs.h"
 #include "utils/relcache.h"
+#include "utils/snapshot.h"
 #include "miscadmin.h"
 
 /* Typedef for callback function for amcheck_lock_relation_and_check */
@@ -26,3 +27,5 @@ extern void amcheck_lock_relation_and_check(Oid indrelid,
 											Oid am_id,
 											IndexDoCheckCallback check,
 											LOCKMODE lockmode, void *state);
+
+extern void check_indcheckxmin(Relation idxrel, Snapshot snapshot);
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 0949c88983a..6231d83ade9 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -442,27 +442,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		if (!state->readonly)
 		{
 			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),
-									   snapshot->xmin))
-				ereport(ERROR,
-						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
-						 errmsg("index \"%s\" cannot be verified using transaction snapshot",
-								RelationGetRelationName(rel))));
+			check_indcheckxmin(state->rel, snapshot);
 		}
 	}
 
-- 
2.43.0

v10-0002-amcheck-brin_index_check-index-structure-check.patchtext/x-patch; charset=US-ASCII; name=v10-0002-amcheck-brin_index_check-index-structure-check.patchDownload
From fd861a9a4b5a2de8b1d79c5b70ee03be91e7c354 Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Mon, 16 Jun 2025 18:11:27 +0300
Subject: [PATCH v10 2/4] amcheck: brin_index_check() - index structure check

Adds a new function brin_index_check() for validating BRIN indexes.
It incudes next checks:
- meta page checks
- revmap pointers is valid and points to index tuples with expected range blkno
- index tuples have expected format
- some special checks for empty_ranges
- every index tuple has corresponding revmap item that points to it (optional)
---
 contrib/amcheck/Makefile                |   5 +-
 contrib/amcheck/amcheck--1.5--1.6.sql   |  18 +
 contrib/amcheck/amcheck.control         |   2 +-
 contrib/amcheck/expected/check_brin.out | 134 ++++
 contrib/amcheck/meson.build             |   4 +
 contrib/amcheck/sql/check_brin.sql      | 100 +++
 contrib/amcheck/t/007_verify_brin.pl    | 301 +++++++++
 contrib/amcheck/verify_brin.c           | 864 ++++++++++++++++++++++++
 doc/src/sgml/amcheck.sgml               |  33 +
 9 files changed, 1458 insertions(+), 3 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.5--1.6.sql
 create mode 100644 contrib/amcheck/expected/check_brin.out
 create mode 100644 contrib/amcheck/sql/check_brin.sql
 create mode 100644 contrib/amcheck/t/007_verify_brin.pl
 create mode 100644 contrib/amcheck/verify_brin.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index 1b7a63cbaa4..bdfb274c89c 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -6,11 +6,12 @@ OBJS = \
 	verify_common.o \
 	verify_gin.o \
 	verify_heapam.o \
-	verify_nbtree.o
+	verify_nbtree.o \
+	verify_brin.o
 
 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
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..0354451c472
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.5--1.6.sql
@@ -0,0 +1,18 @@
+/* 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
+
+
+--
+-- brin_index_check()
+--
+CREATE FUNCTION brin_index_check(index regclass,
+                                 regularpagescheck boolean default false
+)
+    RETURNS VOID
+AS 'MODULE_PATHNAME', 'brin_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION brin_index_check(regclass, boolean) FROM PUBLIC;
\ No newline at end of file
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_brin.out b/contrib/amcheck/expected/check_brin.out
new file mode 100644
index 00000000000..6890fff46bd
--- /dev/null
+++ b/contrib/amcheck/expected/check_brin.out
@@ -0,0 +1,134 @@
+-- helper func
+CREATE OR REPLACE FUNCTION random_string(int) RETURNS text AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::integer, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+-- empty table index should be valid
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- min_max opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multi_min_max opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- bloom opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- inclusion opclass
+CREATE TABLE brintest (id serial PRIMARY KEY, a box);
+CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a)
+SELECT box(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- multiple attributes
+CREATE TABLE brintest (id bigserial, a text) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+ brin_index_check 
+------------------
+ 
+(1 row)
+
+-- cleanup
+DROP TABLE brintest;
+-- cleanup
+DROP FUNCTION random_string;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1f0c347ed54..ba816c2faf0 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -5,6 +5,7 @@ amcheck_sources = files(
   'verify_gin.c',
   'verify_heapam.c',
   'verify_nbtree.c',
+  'verify_brin.c'
 )
 
 if host_system == 'windows'
@@ -27,6 +28,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,
 )
 
@@ -40,6 +42,7 @@ tests += {
       'check_btree',
       'check_gin',
       'check_heap',
+      'check_brin'
     ],
   },
   'tap': {
@@ -50,6 +53,7 @@ tests += {
       't/004_verify_nbtree_unique.pl',
       't/005_pitr.pl',
       't/006_verify_gin.pl',
+      't/007_verify_brin.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_brin.sql b/contrib/amcheck/sql/check_brin.sql
new file mode 100644
index 00000000000..1c97b370cac
--- /dev/null
+++ b/contrib/amcheck/sql/check_brin.sql
@@ -0,0 +1,100 @@
+-- helper func
+CREATE OR REPLACE FUNCTION random_string(int) RETURNS text AS $$
+SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::integer, 1), '') FROM generate_series(1, $1);
+$$ LANGUAGE sql;
+
+
+-- empty table index should be valid
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+-- min_max opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- multi_min_max opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_minmax_multi_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+
+
+-- bloom opclass
+CREATE TABLE brintest (a bigint) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT x FROM generate_series(1,100000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE a > 20000 AND a < 40000;
+SELECT brin_index_check('brintest_idx', true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a int8_bloom_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- inclusion opclass
+CREATE TABLE brintest (id serial PRIMARY KEY, a box);
+CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a)
+SELECT box(point(random() * 1000, random() * 1000), point(random() * 1000, random() * 1000))
+FROM generate_series(1, 10000);
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 2000 AND id < 4000;
+
+SELECT brin_index_check('brintest_idx', true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (a box_inclusion_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- multiple attributes
+CREATE TABLE brintest (id bigserial, a text) WITH (fillfactor = 10);
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
+INSERT INTO brintest (a) SELECT random_string((x % 100)) FROM generate_series(1,3000) x;
+-- create some empty ranges
+DELETE FROM brintest WHERE id > 1500 AND id < 2500;
+SELECT brin_index_check('brintest_idx', true);
+
+-- rebuild index
+DROP INDEX brintest_idx;
+CREATE INDEX brintest_idx ON brintest USING brin (id int8_minmax_ops, a text_minmax_ops) WITH (pages_per_range = 2);
+SELECT brin_index_check('brintest_idx', true);
+-- cleanup
+DROP TABLE brintest;
+
+
+-- cleanup
+DROP FUNCTION random_string;
\ No newline at end of file
diff --git a/contrib/amcheck/t/007_verify_brin.pl b/contrib/amcheck/t/007_verify_brin.pl
new file mode 100644
index 00000000000..c4073f9bdcc
--- /dev/null
+++ b/contrib/amcheck/t/007_verify_brin.pl
@@ -0,0 +1,301 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+my $node;
+my $blksize;
+my $meta_page_blkno = 0;
+
+#
+# Test set-up
+#
+$node = PostgreSQL::Test::Cluster->new('test');
+$node->init(no_data_checksums => 1);
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$blksize = int($node->safe_psql('postgres', 'SHOW block_size;'));
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Tests
+
+# Test flow:
+# - create all necessary relations and indexes for all test cases
+# - stop the node
+# - insert corruptions for all test cases
+# - start the node
+# - assertions
+#
+# This way we avoid waiting for the node to restart for each test, which speeds up the tests.
+
+my @tests = (
+    {
+        # invalid meta page type
+
+        find     => pack('S', 0xF091),
+        replace  => pack('S', 0xAAAA),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # invalid meta page magic word
+
+        find     => pack('L', 0xA8109CFA),
+        replace  => pack('L', 0xBB109CFB),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid meta page index version
+
+        find     => pack('L*', 0xA8109CFA, 1),
+        replace  => pack('L*', 0xA8109CFA, 2),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # pages_per_range above upper limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128),
+        replace  => pack('L*', 0xA8109CFA, 1, 131073),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted')
+    },
+    {
+        # last_revmap_page below lower limit
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 0),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+
+        # last_revmap_page beyond index relation size
+
+        find     => pack('L*', 0xA8109CFA, 1, 128, 1),
+        replace  => pack('L*', 0xA8109CFA, 1, 128, 100),
+        blkno    => $meta_page_blkno,
+        expected => wrap('metapage is corrupted'),
+    },
+    {
+        # invalid revmap page type
+
+        find     => pack('S', 0xF092),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap page is expected at block 1, last revmap page 1'),
+    },
+    {
+        # revmap item points beyond index relation size
+        # replace (2,1) with (100,1)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 100, 1),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item points to a non existing block 100, '
+            . 'index max block 2. Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid regular page type
+
+        find     => pack('S', 0xF093),
+        replace  => pack('S', 0xAAAA),
+        blkno    => 2, # regular page
+        expected => wrap('revmap item points to the page which is not regular (blkno: 2). '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # revmap item points beyond regular page max offset
+        # replace (2,1) with (2,2)
+
+        find     => pack('S*', 0, 2, 1),
+        replace  => pack('S*', 0, 2, 2),
+        blkno    => 1, # revmap page
+        expected => wrap('revmap item offset number 2 is greater than regular page 2 max offset 1. '
+            . 'Range blkno: 0, revmap item: (1,0)')
+    },
+    {
+        # invalid index tuple range blkno
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 1, 0xA8, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('index tuple has invalid blkno 1. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # range beyond the table size and is not empty
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x88, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('the range is beyond the table size, but is not marked as empty, table size: 0 blocks. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # corrupt index tuple data offset
+        # here  0x00, 0x00, 0x00 is padding and '.' is varlena len byte
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '(.)' . 'aaaaa',
+        replace    => pack('LCCCC', 0, 0x1F, 0x00, 0x00, 0x00) . '$1' . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/index tuple header length 31 is greater than tuple len ..\. \QRange blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # empty range index tuple doesn't have null bitmap
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0x28, 0x01),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple doesn\'t have null bitmap. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple all_nulls -> false
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x00),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with allnulls is false. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # empty range index tuple has_nulls -> true
+
+        find     => pack('LCC', 0, 0xA8, 0x01),
+        replace  => pack('LCC', 0, 0xA8, 0x03),
+        blkno    => 2, # regular page
+        expected => wrap('empty range index tuple attribute 0 with hasnulls is true. '
+            . 'Range blkno: 0, revmap item: (1,0), index tuple: (2,1)')
+    },
+    {
+        # invalid index tuple data
+        # replace varlena len with FF - should work with any endianness
+
+        find       => pack('LCCCC', 0, 0x08, 0x00, 0x00, 0x00) . '.' . 'aaaaa',
+        replace    => pack('LCCCCC', 0, 0x08, 0x00, 0x00, 0x00, 0xFF) . 'aaaaa',
+        blkno      => 2, # regular page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => qr/attribute 0 stored value 0 with length -1 ends at offset 127 beyond total tuple length ..\.\Q Range blkno: 0, revmap item: (1,0), index tuple: (2,1)\E/
+    },
+    {
+        # orphan index tuple
+        # replace valid revmap item with (0,0)
+
+        find       => pack('S*', 0, 2, 1),
+        replace    => pack('S*', 0, 0, 0),
+        blkno      => 1, # revmap page
+        table_data => sub {
+            my ($test_struct) = @_;
+            return qq(INSERT INTO $test_struct->{table_name} (a) VALUES ('aaaaa'););
+        },
+        expected   => wrap("revmap doesn't point to index tuple. Range blkno: 0, revmap item: (1,0), index tuple: (2,1)")
+    }
+);
+
+
+# init test data
+my $i = 1;
+foreach my $test_struct (@tests) {
+
+    $test_struct->{table_name} = 't' . $i++;
+    $test_struct->{index_name} = $test_struct->{table_name} . '_brin_idx';
+
+    my $test_data_sql = '';
+    if (exists $test_struct->{table_data}) {
+        $test_data_sql = $test_struct->{table_data}->($test_struct);
+    }
+
+    $node->safe_psql('postgres', qq(
+        CREATE TABLE $test_struct->{table_name} (a TEXT);
+        $test_data_sql
+        CREATE INDEX $test_struct->{index_name} ON $test_struct->{table_name} USING BRIN (a);
+    ));
+
+    $test_struct->{relpath} = relation_filepath($test_struct->{index_name});
+}
+
+# corrupt index
+$node->stop;
+
+foreach my $test_struct (@tests) {
+    string_replace_block(
+        $test_struct->{relpath},
+        $test_struct->{find},
+        $test_struct->{replace},
+        $test_struct->{blkno}
+    );
+}
+
+# assertions
+$node->start;
+
+foreach my $test_struct (@tests) {
+    my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT brin_index_check('$test_struct->{index_name}', true)));
+    like($stderr, $test_struct->{expected});
+}
+
+
+# Helpers
+
+# 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";
+}
+
+sub string_replace_block {
+    my ($filename, $find, $replace, $blkno) = @_;
+
+    my $fh;
+    open($fh, '+<', $filename) or BAIL_OUT("open failed: $!");
+    binmode $fh;
+
+    my $offset = $blkno * $blksize;
+    my $buffer;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!");
+
+    $buffer =~ s/$find/'"' . $replace . '"'/gee;
+
+    sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!");
+    syswrite($fh, $buffer) or BAIL_OUT("write failed: $!");
+
+    close($fh) or BAIL_OUT("close failed: $!");
+
+    return;
+}
+
+sub wrap
+{
+    my $input = @_;
+    return qr/\Q$input\E/
+}
+
+done_testing();
\ No newline at end of file
diff --git a/contrib/amcheck/verify_brin.c b/contrib/amcheck/verify_brin.c
new file mode 100644
index 00000000000..183d189685c
--- /dev/null
+++ b/contrib/amcheck/verify_brin.c
@@ -0,0 +1,864 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_brin.c
+ *	  Functions to check postgresql brin indexes for corruption
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_brin.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/transam.h"
+#include "access/brin.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_operator.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/smgr.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "utils/lsyscache.h"
+#include "verify_common.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+
+
+PG_FUNCTION_INFO_V1(brin_index_check);
+
+typedef struct BrinCheckState
+{
+
+	/* Check arguments */
+
+	bool		regularpagescheck;
+
+	/* BRIN check common fields */
+
+	Relation	idxrel;
+	Relation	heaprel;
+	BrinDesc   *bdesc;
+	int			natts;
+	BlockNumber pagesPerRange;
+
+	/* Index structure check fields */
+
+	BufferAccessStrategy checkstrategy;
+	BlockNumber idxnblocks;
+	BlockNumber heapnblocks;
+	BlockNumber lastRevmapPage;
+	/* Current range blkno */
+	BlockNumber rangeBlkno;
+	/* Current revmap item */
+	BlockNumber revmapBlk;
+	Buffer		revmapbuf;
+	Page		revmappage;
+	uint32		revmapidx;
+	/* Current index tuple */
+	BlockNumber regpageBlk;
+	Buffer		regpagebuf;
+	Page		regpage;
+	OffsetNumber regpageoffset;
+
+}			BrinCheckState;
+
+static void brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly);
+
+static void check_brin_index_structure(BrinCheckState * pState);
+
+static void check_meta(BrinCheckState * state);
+
+static void check_revmap(BrinCheckState * state);
+
+static void check_revmap_item(BrinCheckState * state);
+
+static void check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp);
+
+static void check_regular_pages(BrinCheckState * state);
+
+static bool revmap_points_to_index_tuple(BrinCheckState * state);
+
+static ItemId PageGetItemIdCareful(BrinCheckState * state);
+
+static void brin_check_ereport(BrinCheckState * state, const char *fmt);
+
+static void revmap_item_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_ereport(BrinCheckState * state, const char *fmt);
+
+static void index_tuple_only_ereport(BrinCheckState * state, const char *fmt);
+
+
+Datum
+brin_index_check(PG_FUNCTION_ARGS)
+{
+	Oid			indrelid = PG_GETARG_OID(0);
+	BrinCheckState *state = palloc0(sizeof(BrinCheckState));
+
+	state->regularpagescheck = PG_GETARG_BOOL(1);
+
+	amcheck_lock_relation_and_check(indrelid,
+									BRIN_AM_OID,
+									brin_check,
+									ShareUpdateExclusiveLock,
+									state);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Main check function
+ */
+static void
+brin_check(Relation idxrel, Relation heaprel, void *callback_state, bool readonly)
+{
+	BrinCheckState *state = (BrinCheckState *) callback_state;
+
+	/* Initialize check common fields */
+	state->idxrel = idxrel;
+	state->heaprel = heaprel;
+	state->bdesc = brin_build_desc(idxrel);
+	state->natts = state->bdesc->bd_tupdesc->natts;
+
+
+	check_brin_index_structure(state);
+
+
+	brin_free_desc(state->bdesc);
+}
+
+/*
+ * Check that index has expected structure
+ *
+ *  Some check expectations:
+ * - we hold ShareUpdateExclusiveLock, so revmap could not be extended (i.e. no evacuation) while check as well as
+ *   all regular pages should stay regular and ranges could not be summarized and desummarized.
+ *   Nevertheless, concurrent updates could lead to new regular page allocations
+ *   and moving of index tuples.
+ * - if revmap pointer is valid there should be valid index tuple it points to.
+ * - there are no orphan index tuples (if there is an index tuple, the revmap item points to this tuple also must exist)
+ * - it's possible to encounter placeholder tuples (as a result of crash)
+ * - it's possible to encounter new pages instead of regular (as a result of crash)
+ * - it's possible to encounter pages with evacuation bit (as a result of crash)
+ *
+ */
+static void
+check_brin_index_structure(BrinCheckState * state)
+{
+	/* Index structure check fields initialization */
+	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
+
+	check_meta(state);
+
+	/* Check revmap first, blocks: [1, lastRevmapPage] */
+	check_revmap(state);
+
+
+	if (state->regularpagescheck)
+	{
+		/* Check regular pages, blocks: [lastRevmapPage + 1, idxnblocks] */
+		check_regular_pages(state);
+	}
+
+}
+
+/* Meta page check and save some data for the further check */
+static void
+check_meta(BrinCheckState * state)
+{
+	Buffer		metabuf;
+	Page		metapage;
+	BrinMetaPageData *metadata;
+
+	/* Meta page check */
+	metabuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, BRIN_METAPAGE_BLKNO, RBM_NORMAL,
+								 state->checkstrategy);
+	LockBuffer(metabuf, BUFFER_LOCK_SHARE);
+	metapage = BufferGetPage(metabuf);
+	metadata = (BrinMetaPageData *) PageGetContents(metapage);
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	if (!BRIN_IS_META_PAGE(metapage) ||
+		metadata->brinMagic != BRIN_META_MAGIC ||
+		metadata->brinVersion != BRIN_CURRENT_VERSION ||
+		metadata->pagesPerRange < 1 || metadata->pagesPerRange > BRIN_MAX_PAGES_PER_RANGE ||
+		metadata->lastRevmapPage <= BRIN_METAPAGE_BLKNO || metadata->lastRevmapPage >= state->idxnblocks)
+	{
+		brin_check_ereport(state, "metapage is corrupted");
+	}
+
+	state->lastRevmapPage = metadata->lastRevmapPage;
+	state->pagesPerRange = metadata->pagesPerRange;
+	UnlockReleaseBuffer(metabuf);
+}
+
+/*
+ * This is a main part of the brin index structure check.
+ * We walk revmap page by page from the beginning and check every revmap item and
+ * every index tuple pointed from the revmap.
+ */
+static void
+check_revmap(BrinCheckState * state)
+{
+	Relation	idxrel = state->idxrel;
+	BlockNumber lastRevmapPage = state->lastRevmapPage;
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	state->rangeBlkno = 0;
+	state->regpagebuf = InvalidBuffer;
+	state->heapnblocks = RelationGetNumberOfBlocks(state->heaprel);
+
+
+	/*
+	 * Prepare stream data for revmap walk. It is safe to use batchmode as
+	 * block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING;
+	/* First revmap page is right after meta page */
+	stream_data.current_blocknum = BRIN_METAPAGE_BLKNO + 1;
+	stream_data.last_exclusive = lastRevmapPage + 1;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	/* Walk each revmap page */
+	while ((state->revmapbuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		state->revmapBlk = BufferGetBlockNumber(state->revmapbuf);
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+		state->revmappage = BufferGetPage(state->revmapbuf);
+
+		/*
+		 * Pages with block numbers in [1, lastRevmapPage] should be revmap
+		 * pages
+		 */
+		if (!BRIN_IS_REVMAP_PAGE(state->revmappage))
+		{
+			brin_check_ereport(state, psprintf("revmap page is expected at block %u, last revmap page %u",
+											   state->revmapBlk,
+											   lastRevmapPage));
+		}
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/* Walk and check all brin tuples from the current revmap page */
+		state->revmapidx = 0;
+		while (state->revmapidx < REVMAP_PAGE_MAXITEMS)
+		{
+			CHECK_FOR_INTERRUPTS();
+
+			/* Check revmap item */
+			check_revmap_item(state);
+
+			state->rangeBlkno += state->pagesPerRange;
+			state->revmapidx++;
+		}
+
+		elog(DEBUG3, "Complete revmap page check: %d", state->revmapBlk);
+
+		ReleaseBuffer(state->revmapbuf);
+	}
+
+	read_stream_end(stream);
+
+	if (BufferIsValid(state->regpagebuf))
+	{
+		ReleaseBuffer(state->regpagebuf);
+	}
+}
+
+/*
+ * Check revmap item.
+ *
+ * We check revmap item pointer itself and if it is ok we check the index tuple it points to.
+ *
+ * To avoid deadlock we need to unlock revmap page before locking regular page,
+ * so when we get the lock on the regular page our index tuple pointer may no longer be relevant.
+ * So for some checks before reporting an error we need to make sure that our pointer is still relevant and if it's not - retry.
+ */
+static void
+check_revmap_item(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *iptr;
+	ItemId		lp;
+	BrinTuple  *tup;
+	Relation	idxrel = state->idxrel;
+
+	/* Loop to retry revmap item check if there was a concurrent update. */
+	for (;;)
+	{
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+
+		contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+		revmaptids = contents->rm_tids;
+		/* Pointer for the range with start at state->rangeBlkno */
+		iptr = revmaptids + state->revmapidx;
+
+		/* At first check revmap item pointer */
+
+		/*
+		 * Tuple pointer is invalid means range isn't summarized, just move
+		 * further
+		 */
+		if (!ItemPointerIsValid(iptr))
+		{
+			elog(DEBUG3, "Range %u is not summarized", state->rangeBlkno);
+			LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+			break;
+		}
+
+		/*
+		 * Pointer is valid, it should points to index tuple for the range
+		 * with blkno rangeBlkno. Remember it and unlock revmap page to avoid
+		 * deadlock
+		 */
+		state->regpageBlk = ItemPointerGetBlockNumber(iptr);
+		state->regpageoffset = ItemPointerGetOffsetNumber(iptr);
+
+		LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+
+		/*
+		 * Check if the regpage block number is greater than the relation
+		 * size. To avoid fetching the number of blocks for each tuple, use
+		 * cached value first
+		 */
+		if (state->regpageBlk >= state->idxnblocks)
+		{
+			/*
+			 * Regular pages may have been added, so refresh idxnblocks and
+			 * recheck
+			 */
+			state->idxnblocks = RelationGetNumberOfBlocks(idxrel);
+			if (state->regpageBlk >= state->idxnblocks)
+			{
+				revmap_item_ereport(state,
+									psprintf("revmap item points to a non existing block %u, index max block %u",
+											 state->regpageBlk,
+											 state->idxnblocks - 1));
+			}
+		}
+
+		/*
+		 * To avoid some pin/unpin cycles we cache last used regular page.
+		 * Check if we need different regular page and fetch it.
+		 */
+		if (!BufferIsValid(state->regpagebuf) || BufferGetBlockNumber(state->regpagebuf) != state->regpageBlk)
+		{
+			if (BufferIsValid(state->regpagebuf))
+			{
+				ReleaseBuffer(state->regpagebuf);
+			}
+			state->regpagebuf = ReadBufferExtended(idxrel, MAIN_FORKNUM, state->regpageBlk, RBM_NORMAL,
+												   state->checkstrategy);
+		}
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Revmap should always point to a regular page */
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			revmap_item_ereport(state,
+								psprintf("revmap item points to the page which is not regular (blkno: %u)",
+										 state->regpageBlk));
+
+		}
+
+		/* Check item offset is valid */
+		if (state->regpageoffset > PageGetMaxOffsetNumber(state->regpage))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			revmap_item_ereport(state,
+								psprintf("revmap item offset number %u is greater than regular page %u max offset %u",
+										 state->regpageoffset,
+										 state->regpageBlk,
+										 PageGetMaxOffsetNumber(state->regpage)));
+		}
+
+		elog(DEBUG3, "Process range: %u, iptr: (%u,%u)", state->rangeBlkno, state->regpageBlk, state->regpageoffset);
+
+		/*
+		 * Revmap pointer is OK. It points to existing regular page, offset
+		 * also is ok. Let's check index tuple it points to.
+		 */
+
+		lp = PageGetItemIdCareful(state);
+
+		/* Revmap should point to NORMAL tuples only */
+		if (!ItemIdIsUsed(lp))
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, "revmap item points to unused index tuple");
+		}
+
+
+		tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+		/* Check if range block number is as expected */
+		if (tup->bt_blkno != state->rangeBlkno)
+		{
+
+			/* If concurrent update moved our tuple we need to retry */
+			if (!revmap_points_to_index_tuple(state))
+			{
+				LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+				continue;
+			}
+
+			index_tuple_ereport(state, psprintf("index tuple has invalid blkno %u", tup->bt_blkno));
+		}
+
+		/*
+		 * If the range is beyond the table size - the range must be empty.
+		 * It's valid situation for empty table now.
+		 */
+		if (state->rangeBlkno >= state->heapnblocks)
+		{
+			if (!BrinTupleIsEmptyRange(tup))
+			{
+				index_tuple_ereport(state,
+									psprintf("the range is beyond the table size, "
+											 "but is not marked as empty, table size: %u blocks",
+											 state->heapnblocks));
+			}
+		}
+
+		/* Check index tuple itself */
+		check_index_tuple(state, tup, lp);
+
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_UNLOCK);
+		break;
+	}
+}
+
+/*
+ * Check that index tuple has expected structure.
+ *
+ * This function follows the logic performed by brin_deform_tuple().
+ * After this check is complete we are sure that brin_deform_tuple can process it.
+ *
+ * In case of empty range check that for all attributes allnulls are true, hasnulls are false and
+ * there is no data. All core opclasses expect allnulls is true for empty range.
+ */
+static void
+check_index_tuple(BrinCheckState * state, BrinTuple *tuple, ItemId lp)
+{
+
+	char	   *tp;				/* tuple data */
+	uint16		off;
+	bits8	   *nullbits;
+	TupleDesc	disktdesc;
+	int			stored;
+	bool		empty_range = BrinTupleIsEmptyRange(tuple);
+	bool		hasnullbitmap = BrinTupleHasNulls(tuple);
+	uint8		hoff = BrinTupleDataOffset(tuple);
+	uint16		tuplen = ItemIdGetLength(lp);
+
+
+	/* Check that header length is not greater than tuple length */
+	if (hoff > tuplen)
+	{
+		index_tuple_ereport(state, psprintf("index tuple header length %u is greater than tuple len %u", hoff, tuplen));
+	}
+
+	/* If tuple has null bitmap - initialize it */
+	if (hasnullbitmap)
+	{
+		nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple);
+	}
+	else
+	{
+		nullbits = NULL;
+	}
+
+	/* Empty range index tuple checks */
+	if (empty_range)
+	{
+		/* Empty range tuple should have null bitmap */
+		if (!hasnullbitmap)
+		{
+			index_tuple_ereport(state, "empty range index tuple doesn't have null bitmap");
+		}
+
+		Assert(nullbits != NULL);
+
+		/* Check every attribute has allnulls is true and hasnulls is false */
+		for (int attindex = 0; attindex < state->natts; ++attindex)
+		{
+
+			/* Attribute allnulls should be true for empty range */
+			if (att_isnull(attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with allnulls is false",
+											 attindex));
+			}
+
+			/* Attribute hasnulls should be false for empty range */
+			if (!att_isnull(state->natts + attindex, nullbits))
+			{
+				index_tuple_ereport(state,
+									psprintf("empty range index tuple attribute %d with hasnulls is true",
+											 attindex));
+			}
+		}
+
+		/* We are done with empty range tuple */
+		return;
+	}
+
+	/*
+	 * Range is marked as not empty so we can have some data in the tuple.
+	 * Walk all attributes and checks that all stored values fit into the
+	 * tuple
+	 */
+
+	tp = (char *) tuple + BrinTupleDataOffset(tuple);
+	stored = 0;
+	off = 0;
+
+	disktdesc = brin_tuple_tupdesc(state->bdesc);
+
+	for (int attindex = 0; attindex < state->natts; ++attindex)
+	{
+		BrinOpcInfo *opclass = state->bdesc->bd_info[attindex];
+
+		/*
+		 * if allnulls is set we have no data for this attribute, move to the
+		 * next
+		 */
+		if (hasnullbitmap && !att_isnull(attindex, nullbits))
+		{
+			stored += opclass->oi_nstored;
+			continue;
+		}
+
+		/* Walk all stored values for the current attribute */
+		for (int datumno = 0; datumno < opclass->oi_nstored; datumno++)
+		{
+			CompactAttribute *thisatt = TupleDescCompactAttr(disktdesc, stored);
+
+			if (thisatt->attlen == -1)
+			{
+				off = att_pointer_alignby(off,
+										  thisatt->attalignby,
+										  -1,
+										  tp + off);
+			}
+			else
+			{
+				off = att_nominal_alignby(off, thisatt->attalignby);
+			}
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "starts at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+
+			off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+			/* Check that we are still in the tuple */
+			if (hoff + off > tuplen)
+			{
+				index_tuple_ereport(state,
+									psprintf("attribute %u stored value %u with length %d "
+											 "ends at offset %u beyond total tuple length %u",
+											 attindex, datumno, thisatt->attlen, off, tuplen));
+			}
+			stored++;
+		}
+
+	}
+
+}
+
+/*
+ * At the moment we should have been already check that every index
+ * tuple in the regular pages has valid structure and range blkno
+ * (because every normal index tuple must have pointer in the revmap and
+ * we followed every such pointer in check_revmap). So here we just want
+ * to do some additional checks to be sure that there is nothing wrong
+ * with the regular pages [lastRevmapPage + 1, indexnblocks]:
+ *   - there is a pointer in revmap to each NORMAL index tuple
+ *     (no orphans index tuples)
+ *   - all pages have expected type (REGULAR). We can encounter new pages as
+ *     result of crash, so we just skip such pages.
+ */
+static void
+check_regular_pages(BrinCheckState * state)
+{
+	ReadStream *stream;
+	int			stream_flags;
+	ReadStreamBlockNumberCB stream_cb;
+	BlockRangeReadStreamPrivate stream_data;
+
+	/* reset state */
+	state->revmapBlk = InvalidBlockNumber;
+	state->revmapbuf = InvalidBuffer;
+	state->revmapidx = -1;
+	state->regpageBlk = InvalidBlockNumber;
+	state->regpagebuf = InvalidBuffer;
+	state->regpageoffset = InvalidOffsetNumber;
+	state->idxnblocks = RelationGetNumberOfBlocks(state->idxrel);
+
+
+	/*
+	 * Prepare stream data for regular pages walk. It is safe to use batchmode
+	 * as block_range_read_stream_cb takes no locks.
+	 */
+	stream_flags = READ_STREAM_SEQUENTIAL | READ_STREAM_USE_BATCHING | READ_STREAM_FULL;
+	/* First regular page is right after the last revmap page */
+	stream_data.current_blocknum = state->lastRevmapPage + 1;
+	stream_data.last_exclusive = state->idxnblocks;
+
+	stream_cb = block_range_read_stream_cb;
+	stream = read_stream_begin_relation(stream_flags,
+										GetAccessStrategy(BAS_BULKREAD),
+										state->idxrel,
+										MAIN_FORKNUM,
+										stream_cb,
+										&stream_data,
+										0);
+
+	while ((state->regpagebuf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
+	{
+		OffsetNumber maxoff;
+
+		state->regpageBlk = BufferGetBlockNumber(state->regpagebuf);
+		LockBuffer(state->regpagebuf, BUFFER_LOCK_SHARE);
+		state->regpage = BufferGetPage(state->regpagebuf);
+
+		/* Skip new pages */
+		if (PageIsNew(state->regpage))
+		{
+			UnlockReleaseBuffer(state->regpagebuf);
+			continue;
+		}
+
+		if (!BRIN_IS_REGULAR_PAGE(state->regpage))
+		{
+			brin_check_ereport(state, psprintf("expected new or regular page at block %u", state->regpageBlk));
+		}
+
+		/* Check that all NORMAL index tuples within the page are not orphans */
+		maxoff = PageGetMaxOffsetNumber(state->regpage);
+		for (state->regpageoffset = FirstOffsetNumber; state->regpageoffset <= maxoff; state->regpageoffset++)
+		{
+			ItemId		lp;
+			BrinTuple  *tup;
+			BlockNumber revmapBlk;
+
+			lp = PageGetItemIdCareful(state);
+
+			if (ItemIdIsUsed(lp))
+			{
+				tup = (BrinTuple *) PageGetItem(state->regpage, lp);
+
+				/* Get revmap block number for index tuple blkno */
+				revmapBlk = ((tup->bt_blkno / state->pagesPerRange) / REVMAP_PAGE_MAXITEMS) + 1;
+				if (revmapBlk > state->lastRevmapPage)
+				{
+					index_tuple_only_ereport(state, psprintf("no revmap page for the index tuple with blkno %u",
+															 tup->bt_blkno));
+				}
+
+				/* Fetch another revmap page if needed */
+				if (state->revmapBlk != revmapBlk)
+				{
+					if (BlockNumberIsValid(state->revmapBlk))
+					{
+						ReleaseBuffer(state->revmapbuf);
+					}
+					state->revmapBlk = revmapBlk;
+					state->revmapbuf = ReadBufferExtended(state->idxrel, MAIN_FORKNUM, state->revmapBlk, RBM_NORMAL,
+														  state->checkstrategy);
+				}
+
+				state->revmapidx = (tup->bt_blkno / state->pagesPerRange) % REVMAP_PAGE_MAXITEMS;
+				state->rangeBlkno = tup->bt_blkno;
+
+				/* check that revmap item points to index tuple */
+				if (!revmap_points_to_index_tuple(state))
+				{
+					index_tuple_ereport(state, psprintf("revmap doesn't point to index tuple"));
+				}
+
+			}
+		}
+
+		UnlockReleaseBuffer(state->regpagebuf);
+	}
+
+	read_stream_end(stream);
+
+	if (state->revmapbuf != InvalidBuffer)
+	{
+		ReleaseBuffer(state->revmapbuf);
+	}
+}
+
+/*
+ * Check if the revmap item points to the index tuple (regpageBlk, regpageoffset).
+ * We have locked reg page, and lock revmap page here.
+ * It's a valid lock ordering, so no deadlock is possible.
+ */
+static bool
+revmap_points_to_index_tuple(BrinCheckState * state)
+{
+	ItemPointerData *revmaptids;
+	RevmapContents *contents;
+	ItemPointerData *tid;
+	bool		points;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_SHARE);
+	contents = (RevmapContents *) PageGetContents(BufferGetPage(state->revmapbuf));
+	revmaptids = contents->rm_tids;
+	tid = revmaptids + state->revmapidx;
+
+	points = ItemPointerGetBlockNumberNoCheck(tid) == state->regpageBlk &&
+		ItemPointerGetOffsetNumberNoCheck(tid) == state->regpageoffset;
+
+	LockBuffer(state->revmapbuf, BUFFER_LOCK_UNLOCK);
+	return points;
+}
+
+/*
+ * PageGetItemId() wrapper that validates returned line pointer.
+ *
+ * itemId in brin index could be UNUSED or NORMAL.
+ */
+static ItemId
+PageGetItemIdCareful(BrinCheckState * state)
+{
+	Page		page = state->regpage;
+	OffsetNumber offset = state->regpageoffset;
+	ItemId		itemid = PageGetItemId(page, offset);
+
+	if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+		BLCKSZ - MAXALIGN(sizeof(BrinSpecialSpace)))
+		index_tuple_ereport(state,
+							psprintf("line pointer points past end of tuple space in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 )
+			);
+
+	/* Verify that line pointer is LP_NORMAL or LP_UNUSED */
+	if (!((ItemIdIsNormal(itemid) && ItemIdHasStorage(itemid)) ||
+		  (!ItemIdIsUsed(itemid) && !ItemIdHasStorage(itemid))))
+	{
+		index_tuple_ereport(state,
+							psprintf("invalid line pointer storage in index. "
+									 "lp_off=%u, lp_len=%u lp_flags=%u",
+									 ItemIdGetOffset(itemid),
+									 ItemIdGetLength(itemid),
+									 ItemIdGetFlags(itemid)
+									 ));
+	}
+
+	return itemid;
+}
+
+
+/* Report without any additional info */
+static void
+brin_check_ereport(BrinCheckState * state, const char *fmt)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s", RelationGetRelationName(state->idxrel), fmt)));
+}
+
+/* Report with range blkno, revmap item info, index tuple info */
+void
+index_tuple_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u), index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with index tuple info */
+void
+index_tuple_only_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->regpageBlk != InvalidBlockNumber);
+	Assert(state->regpageoffset != InvalidOffsetNumber);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Index tuple: (%u,%u)",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->regpageBlk,
+					state->regpageoffset)));
+}
+
+/* Report with range blkno, revmap item info */
+void
+revmap_item_ereport(BrinCheckState * state, const char *fmt)
+{
+	Assert(state->rangeBlkno != InvalidBlockNumber);
+	Assert(state->revmapBlk != InvalidBlockNumber);
+	Assert(state->revmapidx >= 0 && state->revmapidx < REVMAP_PAGE_MAXITEMS);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("Index %s is corrupted - %s. Range blkno: %u, revmap item: (%u,%u).",
+					RelationGetRelationName(state->idxrel),
+					fmt,
+					state->rangeBlkno,
+					state->revmapBlk,
+					state->revmapidx)));
+}
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 0aff0a6c8c6..2f76af907ec 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -231,6 +231,39 @@ SET client_min_messages = DEBUG1;
   </tip>
 
   <variablelist>
+   <varlistentry>
+    <term>
+     <function>brin_index_check(index regclass, regularpagescheck boolean) returns void</function>
+     <indexterm>
+      <primary>brin_index_check</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <function>brin_index_check</function> tests that its target BRIN index
+      has no structural corruptions. A <literal>ShareUpdateExclusiveLock</literal>
+      is required on the target index by <function>brin_index_check</function>.
+     </para>
+     <para>
+      The following optional arguments are recognized:
+     </para>
+     <variablelist>
+      <varlistentry>
+       <term><literal>regularpagescheck</literal></term>
+       <listitem>
+        <para>
+         If true, check does another run over all regular pages and tries to
+         find some corruptions that was not possible to find during the basic check.
+        </para>
+        <para>
+         Defaults to false.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
    <varlistentry>
     <term>
      <function>
-- 
2.43.0