[PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Started by Pavel Borisovalmost 5 years ago86 messages
#1Pavel Borisov
pashkin.elfe@gmail.com
2 attachment(s)

Hi, hackers!

It seems that if btree index with a unique constraint is corrupted by
duplicates, amcheck now can not catch this. Reindex becomes impossible as
it throws an error but otherwise the index will let the user know that it
is corrupted, and amcheck will tell that the index is clean. So I'd like to
propose a short patch to improve amcheck for checking the unique
constraint. It will output tid's of tuples that are duplicated in the index
(i.e. more than one tid for the same index key is visible) and the user can
easily investigate and delete corresponding table entries.

0001 - is the actual patch, and
0002 - is a temporary hack for testing. It will allow inserting duplicates
in a table even if an index with the exact name "idx" has a unique
constraint (generally it is prohibited to insert). Then a new amcheck will
tell us about these duplicates. It's pity but testing can not be done
automatically, as it needs a core recompile. For testing I'd recommend a
protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

*create table tbl2 (a varchar(50), b varchar(150), c bytea, d
varchar(50));create unique index idx on tbl2(a,b);insert into tbl2 select
i::text::varchar, i::text::varchar, i::text::bytea, i::text::varchar from
generate_series(0,3) as i, generate_series(0,10000) as x;*

So we'll have a generous amount of duplicates in the table and index. Then:

*create extension amcheck;*
*select bt_index_check('idx', true);*

There will be output about each pair of duplicates: index tid's, position
in a posting list (if the index item is deduplicated) and table tid's.

WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 218 and posting 219 (point to heap tid=(126,93) and tid=(126,94))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 219 and posting 220 (point to heap tid=(126,94) and tid=(126,95))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 220 and posting 221 (point to heap tid=(126,95) and tid=(126,96))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 221 and tid=(26,7) posting 0 (point to heap tid=(126,96) and
tid=(126,97)) page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 0 and posting 1 (point to heap tid=(126,97) and tid=(126,98)) page
lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 1 and posting 2 (point to heap tid=(126,98) and tid=(126,99)) page
lsn=0/1B3D420.

Things to notice in the test output:
- cross-page duplicates (when some of them on the one index page and the
other are on the next). (Under patch 0002 they are marked by an additional
message "*INFO: cross page equal keys"* to catch them among the other)

- If we delete table entries corresponding to some duplicates in between
the other duplicates (vacuum should be off), the message for this entry
should disappear but the other duplicates should be detected as previous.

*delete from tbl2 where ctid::text='(126,94)';*
*select bt_index_check('idx', true);*

WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 218 and posting 220 (point to heap tid=(126,93) and tid=(126,95))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 220 and posting 221 (point to heap tid=(126,95) and tid=(126,96))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 221 and tid=(26,7) posting 0 (point to heap tid=(126,96) and
tid=(126,97)) page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 0 and posting 1 (point to heap tid=(126,97) and tid=(126,98)) page
lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 1 and posting 2 (point to heap tid=(126,98) and tid=(126,99)) page
lsn=0/1B3D420.

Caveat: if the first entry on the next index page has a key equal to the
key on a previous page AND all heap tid's corresponding to this entry are
invisible, currently cross-page check can not detect unique constraint
violation between previous index page entry and 2nd, 3d and next current
index page entries. In this case, there would be a message that recommends
doing VACUUM to remove the invisible entries from the index and repeat the
check. (Generally, it is recommended to do vacuum before the check, but for
the testing purpose I'd recommend turning it off to check the detection of
visible-invisible-visible duplicates scenarios)

Your feedback is very much welcome, as usual.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v1-0002-Ignore-unique-constraint-check-on-btree-insert.-P.patchapplication/octet-stream; name=v1-0002-Ignore-unique-constraint-check-on-btree-insert.-P.patchDownload
From a54ab438e911773f18409b2e2b951dc518d715e1 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 11:57:49 +0400
Subject: [PATCH v1 2/2] Ignore unique constraint check on btree insert. Patch
 for testing unique constraint checking in amcheck. Used to construct btree
 index with violated unique constraint which is not generally possible.

Also increase log level of some debug messages in btree amcheck.

XXX Not for merge, testing only !!!!
---
 contrib/amcheck/verify_nbtree.c       |  4 ++--
 src/backend/access/nbtree/nbtinsert.c | 16 ++++++++++++----
 2 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 8a5809f017e..b4f012f5214 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1729,7 +1729,7 @@ bt_target_page_check(BtreeCheckState *state)
 			 */
 			if (state->indexinfo->ii_Unique && rightkey && P_ISLEAF(topaque))
 			{
-				elog(DEBUG2, "check cross page unique condition");
+				elog(INFO, "check cross page unique condition");
 
 				/*
 				 * Make _bt_compare compare only index keys without heap TIDs.
@@ -1741,7 +1741,7 @@ bt_target_page_check(BtreeCheckState *state)
 				/* First key on next page is same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
 				{
-					elog(DEBUG2, "cross page equal keys");
+					elog(INFO, "cross page equal keys");
 					state->target = palloc_btree_page(state,
 													  state->targetblock + 1);
 					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index e3336039125..ffbbf5c63e0 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -570,10 +570,15 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 					 */
 					if (checkUnique == UNIQUE_CHECK_PARTIAL)
 					{
-						if (nbuf != InvalidBuffer)
-							_bt_relbuf(rel, nbuf);
-						*is_unique = false;
-						return InvalidTransactionId;
+						/* FIXME Ugliest crutch to test amcheck */
+						if (strcmp(RelationGetRelationName(rel), "idx") != 0)
+						{
+							if (nbuf != InvalidBuffer)
+								_bt_relbuf(rel, nbuf);
+
+							*is_unique = false;
+							return InvalidTransactionId;
+						}
 					}
 
 					/*
@@ -616,6 +621,9 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 													  SnapshotSelf, NULL))
 					{
 						/* Normal case --- it's still live */
+						/* FIXME Ugliest crutch to test amcheck */
+						if (strcmp(RelationGetRelationName(rel), "idx") == 0)
+							break;
 					}
 					else
 					{
-- 
2.28.0

v1-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchapplication/octet-stream; name=v1-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchDownload
From 68d7fc042e3611ef70e90df057c3dee6ed513727 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 12:26:08 +0400
Subject: [PATCH v1 1/2] Make amcheck checking UNIQUE constraint for btree
 index. On index with unique constraint check that only one table entry
 for the equal keys (including all posting list entries) is visible. Report
 error if not and show all index entries violating the constraint under
 warning level.

Authors: Anastasia Lubennikova <a.lubennikova@postgrespro.ru>, Pavel Borisov <pashkin.elfe@gmail.com>
---
 contrib/amcheck/verify_nbtree.c | 267 +++++++++++++++++++++++++++++++-
 1 file changed, 263 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b8c7793d9e0..8a5809f017e 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -83,6 +83,13 @@ typedef struct BtreeCheckState
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking.
+	 * Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -148,8 +155,20 @@ static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -187,6 +206,7 @@ static ItemId PageGetItemIdCareful(BtreeCheckState *state, BlockNumber block,
 static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 													  IndexTuple itup, bool nonpivot);
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
+static bool errflag; /* Output ERROR at the end of amcheck */
 
 /*
  * bt_index_check(index regclass, heapallindexed boolean)
@@ -449,6 +469,15 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->indexinfo = BuildIndexInfo(state->rel);
+	/*
+	 * We need a snapshot it to check uniqueness of the index
+	 * For better performance, take it once per index check.
+	 */
+	if (state->indexinfo->ii_Unique)
+		state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+	else
+		state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -632,7 +661,16 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
+
+	if (errflag == true)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				errmsg("index \"%s\" is corrupted. There are tuples violating UNIQUE constraint",
+						RelationGetRelationName(state->rel)),
+				errdetail_internal("Details are in the previous log messages under WARNING priority")));
 }
 
 /*
@@ -1006,6 +1044,142 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 								btpo_prev_from_target)));
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+							  tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void bt_report_duplicate(BtreeCheckState *state,
+				 ItemPointer tid, BlockNumber block, OffsetNumber offset,
+				 int posting,
+				 ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+				 int nposting)
+{
+	char	   	*htid,
+				*nhtid,
+				*itid,
+				*nitid = "",
+				*pposting = "",
+				*pnposting = "";
+
+	errflag = true;
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(nexttid),
+					ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) "
+					"page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					(uint32) (state->targetlsn >> 32),
+					(uint32) state->targetlsn)));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+		OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+		OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool has_visible_entry = false;
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid (*lVis_tid))
+				{
+					bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, state->targetblock,
+											offset, i);
+				}
+					*lVis_i = i;
+					*lVis_tid = tid;
+					*lVis_offset = offset;
+					*lVis_block = state->targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list.
+	 * If TID is visible, save info about it for next comparisons in the loop in
+	 * bt_page_check(). If also lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid (*lVis_tid))
+			{
+				bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, state->targetblock,
+											offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = state->targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+									   *lVis_block != state->targetblock)
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness may be violated for index \"%s\": "
+					"First key on an index page %u is equal to the key on the "
+					"previous page %u and is invisible. Cross-page unique "
+					"constraint violation can be missed. Vacuum the table "
+					"and repeat the check.",
+					RelationGetRelationName(state->rel),
+					state->targetblock, *lVis_block)));
+}
+
 /*
  * Function performs the following checks on target page, or pages ancillary to
  * target page:
@@ -1026,6 +1200,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1047,6 +1224,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber offset;
 	OffsetNumber max;
 	BTPageOpaque topaque;
+	/* last visible entry info for checking indexes with unique constraint */
+	int			 lVis_i = -1; /* the position of last visible item for posting
+							   * tuple. for non-posting tuple (-1)
+							   */
+	ItemPointer	 lVis_tid = NULL;
+	BlockNumber	 lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
 
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1446,6 +1630,39 @@ bt_target_page_check(BtreeCheckState *state)
 										(uint32) state->targetlsn)));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking
+		 * heap tuples visibility.
+		 */
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, offset,
+					&lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+				 OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+			/* Invalidate scankey tid to make _bt_compare compare only keys
+			 * in the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+						OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid; /* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1463,12 +1680,14 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1503,6 +1722,43 @@ bt_target_page_check(BtreeCheckState *state)
 											(uint32) (state->targetlsn >> 32),
 											(uint32) state->targetlsn)));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one found
+			 * equal items is visible.
+			 */
+			if (state->indexinfo->ii_Unique && rightkey && P_ISLEAF(topaque))
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  state->targetblock + 1);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+							break;
+
+					itemid = PageGetItemIdCareful(state, state->targetblock + 1,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightfirstoffset,
+									&lVis_i, &lVis_tid, &lVis_offset,
+									&lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1548,9 +1804,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1713,6 +1971,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
-- 
2.28.0

#2Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#1)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, 8 Feb 2021, 14:46 Pavel Borisov <pashkin.elfe@gmail.com wrote:

Hi, hackers!

It seems that if btree index with a unique constraint is corrupted by
duplicates, amcheck now can not catch this. Reindex becomes impossible as
it throws an error but otherwise the index will let the user know that it
is corrupted, and amcheck will tell that the index is clean. So I'd like to
propose a short patch to improve amcheck for checking the unique
constraint. It will output tid's of tuples that are duplicated in the index
(i.e. more than one tid for the same index key is visible) and the user can
easily investigate and delete corresponding table entries.

0001 - is the actual patch, and
0002 - is a temporary hack for testing. It will allow inserting duplicates
in a table even if an index with the exact name "idx" has a unique
constraint (generally it is prohibited to insert). Then a new amcheck will
tell us about these duplicates. It's pity but testing can not be done
automatically, as it needs a core recompile. For testing I'd recommend a
protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

*create table tbl2 (a varchar(50), b varchar(150), c bytea, d
varchar(50));create unique index idx on tbl2(a,b);insert into tbl2 select
i::text::varchar, i::text::varchar, i::text::bytea, i::text::varchar from
generate_series(0,3) as i, generate_series(0,10000) as x;*

So we'll have a generous amount of duplicates in the table and index. Then:

*create extension amcheck;*
*select bt_index_check('idx', true);*

There will be output about each pair of duplicates: index tid's, position
in a posting list (if the index item is deduplicated) and table tid's.

WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 218 and posting 219 (point to heap tid=(126,93) and tid=(126,94))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 219 and posting 220 (point to heap tid=(126,94) and tid=(126,95))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 220 and posting 221 (point to heap tid=(126,95) and tid=(126,96))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 221 and tid=(26,7) posting 0 (point to heap tid=(126,96) and
tid=(126,97)) page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 0 and posting 1 (point to heap tid=(126,97) and tid=(126,98)) page
lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 1 and posting 2 (point to heap tid=(126,98) and tid=(126,99)) page
lsn=0/1B3D420.

Things to notice in the test output:
- cross-page duplicates (when some of them on the one index page and the
other are on the next). (Under patch 0002 they are marked by an additional
message "*INFO: cross page equal keys"* to catch them among the other)

- If we delete table entries corresponding to some duplicates in between
the other duplicates (vacuum should be off), the message for this entry
should disappear but the other duplicates should be detected as previous.

*delete from tbl2 where ctid::text='(126,94)';*
*select bt_index_check('idx', true);*

WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 218 and posting 220 (point to heap tid=(126,93) and tid=(126,95))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 220 and posting 221 (point to heap tid=(126,95) and tid=(126,96))
page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,6)
posting 221 and tid=(26,7) posting 0 (point to heap tid=(126,96) and
tid=(126,97)) page lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 0 and posting 1 (point to heap tid=(126,97) and tid=(126,98)) page
lsn=0/1B3D420.
WARNING: index uniqueness is violated for index "idx": Index tid=(26,7)
posting 1 and posting 2 (point to heap tid=(126,98) and tid=(126,99)) page
lsn=0/1B3D420.

Caveat: if the first entry on the next index page has a key equal to the
key on a previous page AND all heap tid's corresponding to this entry are
invisible, currently cross-page check can not detect unique constraint
violation between previous index page entry and 2nd, 3d and next current
index page entries. In this case, there would be a message that recommends
doing VACUUM to remove the invisible entries from the index and repeat the
check. (Generally, it is recommended to do vacuum before the check, but for
the testing purpose I'd recommend turning it off to check the detection of
visible-invisible-visible duplicates scenarios)

Your feedback is very much welcome, as usual.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

There was typo, I mean, initially"...index will NOT let the user know that
it is corrupted..."

#3Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#1)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Feb 8, 2021, at 2:46 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

0002 - is a temporary hack for testing. It will allow inserting duplicates in a table even if an index with the exact name "idx" has a unique constraint (generally it is prohibited to insert). Then a new amcheck will tell us about these duplicates. It's pity but testing can not be done automatically, as it needs a core recompile. For testing I'd recommend a protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

Thanks Pavel and Anastasia for working on this!

Updating pg_catalog directly is ugly, but the following seems a simpler way to set up a regression test than having to recompile. What do you think?

CREATE TABLE junk (t text);
CREATE UNIQUE INDEX junk_idx ON junk USING btree (t);
INSERT INTO junk (t) VALUES ('fee'), ('fi'), ('fo'), ('fum');
UPDATE pg_catalog.pg_index
SET indisunique = false
WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'junk');
INSERT INTO junk (t) VALUES ('fee'), ('fi'), ('fo'), ('fum');
UPDATE pg_catalog.pg_index
SET indisunique = true
WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'junk');
SELECT * FROM junk;
t
-----
fee
fi
fo
fum
fee
fi
fo
fum
(8 rows)

\d junk
Table "public.junk"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
t | text | | |
Indexes:
"junk_idx" UNIQUE, btree (t)

\d junk_idx
Index "public.junk_idx"
Column | Type | Key? | Definition
--------+------+------+------------
t | text | yes | t
unique, btree, for table "public.junk"


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#4Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#3)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

вт, 9 февр. 2021 г. в 01:46, Mark Dilger <mark.dilger@enterprisedb.com>:

On Feb 8, 2021, at 2:46 AM, Pavel Borisov <pashkin.elfe@gmail.com>

wrote:

0002 - is a temporary hack for testing. It will allow inserting

duplicates in a table even if an index with the exact name "idx" has a
unique constraint (generally it is prohibited to insert). Then a new
amcheck will tell us about these duplicates. It's pity but testing can not
be done automatically, as it needs a core recompile. For testing I'd
recommend a protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

Thanks Pavel and Anastasia for working on this!

Updating pg_catalog directly is ugly, but the following seems a simpler
way to set up a regression test than having to recompile. What do you
think?

Very nice idea, thanks!

I've made a regression test based on it. PFA v.2 of a patch. Now it doesn't
need anything external for testing.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v2-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchapplication/octet-stream; name=v2-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchDownload
From abaa141811273ebcc511218ec3f40713717a282c Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 12:26:08 +0400
Subject: [PATCH v2] Make amcheck checking UNIQUE constraint for btree index.
 On index with unique constraint ake check that only one table entry for the
 equal keys (including all posting list entries) is visible. Report error if
 not and show all index entries violating the constraint under warning level.

Authors: Anastasia Lubennikova <a.lubennikova@postgrespro.ru>, Pavel Borisov <pashkin.elfe@gmail.com>
---
 contrib/amcheck/expected/check_btree.out | 138 +++++++++++
 contrib/amcheck/sql/check_btree.sql      |  24 ++
 contrib/amcheck/verify_nbtree.c          | 278 ++++++++++++++++++++++-
 3 files changed, 436 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 13848b7449b..875ac1355ae 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,149 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+SELECT bt_index_check('bttest_unique_idx', true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 2 (point to heap tid=(0,1) and tid=(0,3)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,3) and tid=(0,4)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and posting 4 (point to heap tid=(0,4) and tid=(0,5)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 4 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 2 (point to heap tid=(4,1) and tid=(4,4)) page lsn=0/4DAD7E0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and posting 3 (point to heap tid=(4,4) and tid=(4,5)) page lsn=0/4DBD070.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 3 and posting 4 (point to heap tid=(4,5) and tid=(4,6)) page lsn=0/4DBD070.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 4 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)) page lsn=0/4DBD070.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness may be violated for index "bttest_unique_idx": Index tid=(5,1) doesn't have visible heap tids and key is equal to the tid=(4,6) posting 4 (points to heap tid=(9,2)). Cross-page unique constraint violation can be missed. Vacuum the table and repeat the check.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,4) and tid=(9,5)) page lsn=0/4DC77A8.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,5) and tid=(9,6)) page lsn=0/4DC77A8.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(9,6) and tid=(10,1)) page lsn=0/4DC77A8.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,5) and tid=(5,6) (point to heap tid=(10,1) and tid=(10,2)) page lsn=0/4DC77A8.
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 1 (point to heap tid=(0,1) and tid=(0,3)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 1 and posting 2 (point to heap tid=(0,3) and tid=(0,4)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,4) and tid=(0,5)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 0 (point to heap tid=(4,1) and tid=(4,4)) page lsn=0/4DC9D58.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 0 and posting 1 (point to heap tid=(4,4) and tid=(4,5)) page lsn=0/4DC9D98.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 1 and posting 2 (point to heap tid=(4,5) and tid=(4,6)) page lsn=0/4DC9D98.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)) page lsn=0/4DC9D98.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 4 and tid=(5,1) (point to heap tid=(9,2) and tid=(9,4)) page lsn=0/4DC4CD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,1) and tid=(5,2) (point to heap tid=(9,4) and tid=(9,5)) page lsn=0/4DC9DD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,5) and tid=(9,6)) page lsn=0/4DC9DD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,6) and tid=(10,1)) page lsn=0/4DC9DD0.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(10,1) and tid=(10,2)) page lsn=0/4DC9DD0.
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..8df296431fb 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,35 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+SELECT bt_index_check('bttest_unique_idx', true);
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b8c7793d9e0..4519b1e0400 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -83,6 +83,13 @@ typedef struct BtreeCheckState
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking.
+	 * Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -148,8 +155,21 @@ static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -187,6 +207,7 @@ static ItemId PageGetItemIdCareful(BtreeCheckState *state, BlockNumber block,
 static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 													  IndexTuple itup, bool nonpivot);
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
+static bool errflag; /* Output ERROR at the end of amcheck */
 
 /*
  * bt_index_check(index regclass, heapallindexed boolean)
@@ -449,6 +470,15 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->indexinfo = BuildIndexInfo(state->rel);
+	/*
+	 * We need a snapshot it to check uniqueness of the index
+	 * For better performance, take it once per index check.
+	 */
+	if (state->indexinfo->ii_Unique)
+		state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+	else
+		state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -632,7 +662,16 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
+
+	if (errflag == true)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				errmsg("index \"%s\" is corrupted. There are tuples violating UNIQUE constraint",
+						RelationGetRelationName(state->rel)),
+				errdetail_internal("Details are in the previous log messages under WARNING priority")));
 }
 
 /*
@@ -1006,6 +1045,152 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 								btpo_prev_from_target)));
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+							  tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void bt_report_duplicate(BtreeCheckState *state,
+				 ItemPointer tid, BlockNumber block, OffsetNumber offset,
+				 int posting,
+				 ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+				 int nposting)
+{
+	char	   	*htid,
+				*nhtid,
+				*itid,
+				*nitid = "",
+				*pposting = "",
+				*pnposting = "";
+
+	errflag = true;
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(nexttid),
+					ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) "
+					"page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					(uint32) (state->targetlsn >> 32),
+					(uint32) state->targetlsn)));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+		BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+		OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool has_visible_entry = false;
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid (*lVis_tid))
+				{
+					bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, i);
+				}
+				/*
+				 * Prevent double reporting unique violation between the posting
+				 * list entries of a first tuple on the page after cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid (*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list.
+	 * If TID is visible, save info about it for next comparisons in the loop in
+	 * bt_page_check(). If also lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid (*lVis_tid))
+			{
+				bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+									   *lVis_block != targetblock)
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness may be violated for index \"%s\": "
+					"Index tid=(%u,%u) doesn't have visible heap tids and key "
+					"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+					"Cross-page unique constraint violation can be missed. "
+					"Vacuum the table and repeat the check.",
+					RelationGetRelationName(state->rel),
+					targetblock, offset,
+					*lVis_block, *lVis_offset, psprintf(" posting %u", *lVis_i),
+					ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+					ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+}
+
 /*
  * Function performs the following checks on target page, or pages ancillary to
  * target page:
@@ -1026,6 +1211,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1047,6 +1235,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber offset;
 	OffsetNumber max;
 	BTPageOpaque topaque;
+	/* last visible entry info for checking indexes with unique constraint */
+	int			 lVis_i = -1; /* the position of last visible item for posting
+							   * tuple. for non-posting tuple (-1)
+							   */
+	ItemPointer	 lVis_tid = NULL;
+	BlockNumber	 lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
 
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1446,6 +1641,39 @@ bt_target_page_check(BtreeCheckState *state)
 										(uint32) state->targetlsn)));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking
+		 * heap tuples visibility.
+		 */
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+					&lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+				 OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+			/* Invalidate scankey tid to make _bt_compare compare only keys
+			 * in the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+						OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid; /* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1463,12 +1691,14 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1503,6 +1733,43 @@ bt_target_page_check(BtreeCheckState *state)
 											(uint32) (state->targetlsn >> 32),
 											(uint32) state->targetlsn)));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one found
+			 * equal items is visible.
+			 */
+			if (state->indexinfo->ii_Unique && rightkey && P_ISLEAF(topaque))
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  state->targetblock + 1);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+							break;
+
+					itemid = PageGetItemIdCareful(state, state->targetblock + 1,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, state->targetblock + 1, rightfirstoffset,
+									&lVis_i, &lVis_tid, &lVis_offset,
+									&lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1548,9 +1815,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1713,6 +1982,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
-- 
2.28.0

#5Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#4)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

To make tests stable I also removed lsn output under warning level. PFA v3
of a patch

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v3-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchapplication/octet-stream; name=v3-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchDownload
From 1dcf0206cabcaf967de28bc95b28f8587d36ffaa Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 12:26:08 +0400
Subject: [PATCH v3] Make amcheck checking UNIQUE constraint for btree index.
 On index with unique constraint ake check that only one table entry for the
 equal keys (including all posting list entries) is visible. Report error if
 not and show all index entries violating the constraint under warning level.

Authors: Anastasia Lubennikova <a.lubennikova@postgrespro.ru>, Pavel Borisov <pashkin.elfe@gmail.com>
---
 contrib/amcheck/expected/check_btree.out | 138 ++++++++++++
 contrib/amcheck/sql/check_btree.sql      |  24 ++
 contrib/amcheck/verify_nbtree.c          | 275 ++++++++++++++++++++++-
 3 files changed, 433 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 13848b7449b..fe4a958b1b0 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,149 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+SELECT bt_index_check('bttest_unique_idx', true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 2 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and posting 4 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 4 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 2 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and posting 3 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 3 and posting 4 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 4 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness may be violated for index "bttest_unique_idx": Index tid=(5,1) doesn't have visible heap tids and key is equal to the tid=(4,6) posting 4 (points to heap tid=(9,2)). Cross-page unique constraint violation can be missed. Vacuum the table and repeat the check.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,5) and tid=(5,6) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 1 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 1 and posting 2 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 0 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 0 and posting 1 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 1 and posting 2 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 4 and tid=(5,1) (point to heap tid=(9,2) and tid=(9,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,1) and tid=(5,2) (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..8df296431fb 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,35 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+SELECT bt_index_check('bttest_unique_idx', true);
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b8c7793d9e0..6136cf1a22b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -83,6 +83,13 @@ typedef struct BtreeCheckState
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking.
+	 * Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -148,8 +155,21 @@ static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -187,6 +207,7 @@ static ItemId PageGetItemIdCareful(BtreeCheckState *state, BlockNumber block,
 static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 													  IndexTuple itup, bool nonpivot);
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
+static bool errflag; /* Output ERROR at the end of amcheck */
 
 /*
  * bt_index_check(index regclass, heapallindexed boolean)
@@ -449,6 +470,15 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->indexinfo = BuildIndexInfo(state->rel);
+	/*
+	 * We need a snapshot it to check uniqueness of the index
+	 * For better performance, take it once per index check.
+	 */
+	if (state->indexinfo->ii_Unique)
+		state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+	else
+		state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -632,7 +662,16 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
+
+	if (errflag == true)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				errmsg("index \"%s\" is corrupted. There are tuples violating UNIQUE constraint",
+						RelationGetRelationName(state->rel)),
+				errdetail_internal("Details are in the previous log messages under WARNING priority")));
 }
 
 /*
@@ -1006,6 +1045,149 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 								btpo_prev_from_target)));
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+							  tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void bt_report_duplicate(BtreeCheckState *state,
+				 ItemPointer tid, BlockNumber block, OffsetNumber offset,
+				 int posting,
+				 ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+				 int nposting)
+{
+	char	   	*htid,
+				*nhtid,
+				*itid,
+				*nitid = "",
+				*pposting = "",
+				*pnposting = "";
+
+	errflag = true;
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(nexttid),
+					ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s).",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid)));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+		BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+		OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool has_visible_entry = false;
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid (*lVis_tid))
+				{
+					bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, i);
+				}
+				/*
+				 * Prevent double reporting unique violation between the posting
+				 * list entries of a first tuple on the page after cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid (*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list.
+	 * If TID is visible, save info about it for next comparisons in the loop in
+	 * bt_page_check(). If also lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid (*lVis_tid))
+			{
+				bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+									   *lVis_block != targetblock)
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness may be violated for index \"%s\": "
+					"Index tid=(%u,%u) doesn't have visible heap tids and key "
+					"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+					"Cross-page unique constraint violation can be missed. "
+					"Vacuum the table and repeat the check.",
+					RelationGetRelationName(state->rel),
+					targetblock, offset,
+					*lVis_block, *lVis_offset, psprintf(" posting %u", *lVis_i),
+					ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+					ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+}
+
 /*
  * Function performs the following checks on target page, or pages ancillary to
  * target page:
@@ -1026,6 +1208,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1047,6 +1232,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber offset;
 	OffsetNumber max;
 	BTPageOpaque topaque;
+	/* last visible entry info for checking indexes with unique constraint */
+	int			 lVis_i = -1; /* the position of last visible item for posting
+							   * tuple. for non-posting tuple (-1)
+							   */
+	ItemPointer	 lVis_tid = NULL;
+	BlockNumber	 lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
 
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1446,6 +1638,39 @@ bt_target_page_check(BtreeCheckState *state)
 										(uint32) state->targetlsn)));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking
+		 * heap tuples visibility.
+		 */
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+					&lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+				 OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+			/* Invalidate scankey tid to make _bt_compare compare only keys
+			 * in the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+						OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid; /* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1463,12 +1688,14 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1503,6 +1730,43 @@ bt_target_page_check(BtreeCheckState *state)
 											(uint32) (state->targetlsn >> 32),
 											(uint32) state->targetlsn)));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one found
+			 * equal items is visible.
+			 */
+			if (state->indexinfo->ii_Unique && rightkey && P_ISLEAF(topaque))
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  state->targetblock + 1);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+							break;
+
+					itemid = PageGetItemIdCareful(state, state->targetblock + 1,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, state->targetblock + 1, rightfirstoffset,
+									&lVis_i, &lVis_tid, &lVis_offset,
+									&lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1548,9 +1812,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1713,6 +1979,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
-- 
2.28.0

#6Zhihong Yu
zyu@yugabyte.com
In reply to: Pavel Borisov (#4)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi,
Minor comment:

+ if (errflag == true)
+ ereport(ERROR,

I think 'if (errflag)' should suffice.

Cheers

On Tue, Feb 9, 2021 at 10:44 AM Pavel Borisov <pashkin.elfe@gmail.com>
wrote:

Show quoted text

вт, 9 февр. 2021 г. в 01:46, Mark Dilger <mark.dilger@enterprisedb.com>:

On Feb 8, 2021, at 2:46 AM, Pavel Borisov <pashkin.elfe@gmail.com>

wrote:

0002 - is a temporary hack for testing. It will allow inserting

duplicates in a table even if an index with the exact name "idx" has a
unique constraint (generally it is prohibited to insert). Then a new
amcheck will tell us about these duplicates. It's pity but testing can not
be done automatically, as it needs a core recompile. For testing I'd
recommend a protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

Thanks Pavel and Anastasia for working on this!

Updating pg_catalog directly is ugly, but the following seems a simpler
way to set up a regression test than having to recompile. What do you
think?

Very nice idea, thanks!

I've made a regression test based on it. PFA v.2 of a patch. Now it
doesn't need anything external for testing.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

#7Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#4)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Feb 9, 2021, at 10:43 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

вт, 9 февр. 2021 г. в 01:46, Mark Dilger <mark.dilger@enterprisedb.com>:

On Feb 8, 2021, at 2:46 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

0002 - is a temporary hack for testing. It will allow inserting duplicates in a table even if an index with the exact name "idx" has a unique constraint (generally it is prohibited to insert). Then a new amcheck will tell us about these duplicates. It's pity but testing can not be done automatically, as it needs a core recompile. For testing I'd recommend a protocol similar to the following:

- Apply patch 0002
- Set autovaccum = off in postgresql.conf

Thanks Pavel and Anastasia for working on this!

Updating pg_catalog directly is ugly, but the following seems a simpler way to set up a regression test than having to recompile. What do you think?

Very nice idea, thanks!
I've made a regression test based on it. PFA v.2 of a patch. Now it doesn't need anything external for testing.

If bt_index_check() and bt_index_parent_check() are to have this functionality, shouldn't there be an option controlling it much as the option (heapallindexed boolean) controls checking whether all entries in the heap are indexed in the btree? It seems inconsistent to have an option to avoid checking the heap for that, but not for this. Alternately, this might even be better coded as its own function, named something like bt_unique_index_check() perhaps. I hope Peter might advise?

The regression test you provided is not portable. I am getting lots of errors due to differing output of the form "page lsn=0/4DAD7E0". You might turn this into a TAP test and use a regular expression to check the output.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#8Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#7)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

The regression test you provided is not portable. I am getting lots of
errors due to differing output of the form "page lsn=0/4DAD7E0". You might
turn this into a TAP test and use a regular expression to check the output.

May I ask you to ensure you used v3 of a patch to check? I've made tests
portable in v3, probably, you've checked not the last version.

Thanks for your attention to the patch
--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

#9Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#8)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mar 1, 2021, at 12:05 PM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

The regression test you provided is not portable. I am getting lots of errors due to differing output of the form "page lsn=0/4DAD7E0". You might turn this into a TAP test and use a regular expression to check the output.
May I ask you to ensure you used v3 of a patch to check? I've made tests portable in v3, probably, you've checked not the last version.

Yes, my review was of v2. Updating to v3, I see that the test passes on my laptop. It still looks brittle to have all the tid values in the test output, but it does pass.

Thanks for your attention to the patch

Thanks for the patch!


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#10Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#7)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

If bt_index_check() and bt_index_parent_check() are to have this
functionality, shouldn't there be an option controlling it much as the
option (heapallindexed boolean) controls checking whether all entries in
the heap are indexed in the btree? It seems inconsistent to have an option
to avoid checking the heap for that, but not for this. Alternately, this
might even be better coded as its own function, named something like
bt_unique_index_check() perhaps. I hope Peter might advise?

As for heap checking, my reasoning was that we can not check whether a
unique constraint violated by the index, without checking heap tuple
visibility. I.e. we can have many identical index entries without
uniqueness violated if only one of them corresponds to a visible heap
tuple. So heap checking included in my patch is _necessary_ for unique
constraint checking, it should not have an option to be disabled,
otherwise, the only answer we can get is that unique constraint MAY be
violated and may not be, which is quite useless. If you meant something
different, please elaborate.

I can try to rewrite unique constraint checking to be done after all others
check but I suppose it's the performance considerations are that made
previous amcheck routines to do many checks simultaneously. I tried to
stick to this practice. It's also not so elegant to duplicate much code to
make uniqueness checks independently and the resulting patch will be much
bigger and harder to review.

Anyway, your and Peter's further considerations are always welcome.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mark Dilger (#9)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Mark Dilger <mark.dilger@enterprisedb.com> writes:

Yes, my review was of v2. Updating to v3, I see that the test passes on my laptop. It still looks brittle to have all the tid values in the test output, but it does pass.

Hm, anyone tried it on 32-bit hardware?

regards, tom lane

#12Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#10)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mar 1, 2021, at 12:23 PM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

If bt_index_check() and bt_index_parent_check() are to have this functionality, shouldn't there be an option controlling it much as the option (heapallindexed boolean) controls checking whether all entries in the heap are indexed in the btree? It seems inconsistent to have an option to avoid checking the heap for that, but not for this. Alternately, this might even be better coded as its own function, named something like bt_unique_index_check() perhaps. I hope Peter might advise?

As for heap checking, my reasoning was that we can not check whether a unique constraint violated by the index, without checking heap tuple visibility. I.e. we can have many identical index entries without uniqueness violated if only one of them corresponds to a visible heap tuple. So heap checking included in my patch is _necessary_ for unique constraint checking, it should not have an option to be disabled, otherwise, the only answer we can get is that unique constraint MAY be violated and may not be, which is quite useless. If you meant something different, please elaborate.

I completely agree that checking uniqueness requires looking at the heap, but I don't agree that every caller of bt_index_check on an index wants that particular check to be performed. There are multiple ways in which an index might be corrupt, and Peter wrote the code to only check some of them by default, with options to expand the checks to other things. This is why heapallindexed is optional. If you don't want to pay the price of checking all entries in the heap against the btree, you don't have to.

I'm not against running uniqueness checks on unique indexes. It seems fairly normal that a user would want that. Perhaps the option should default to 'true' if unspecified? But having no option at all seems to run contrary to how the other functionality is structured.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#13Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#12)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

I completely agree that checking uniqueness requires looking at the heap,
but I don't agree that every caller of bt_index_check on an index wants
that particular check to be performed. There are multiple ways in which an
index might be corrupt, and Peter wrote the code to only check some of them
by default, with options to expand the checks to other things. This is why
heapallindexed is optional. If you don't want to pay the price of checking
all entries in the heap against the btree, you don't have to.

I've got the idea and revised the patch accordingly. Thanks!
Pfa v4 of a patch. I've added an optional argument to allow uniqueness
checks for the unique indexes.
Also, I added a test variant to make them work on 32-bit systems.
Unfortunately, converting the regression test to TAP would be a pain for
me. Hope it can be used now as a 2-variant regression test for 32 and 64
bit systems.

Thank you for your consideration!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v4-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchapplication/octet-stream; name=v4-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patchDownload
From 34ed10d11a479cbab8afed224f1718de099d0f35 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 12:26:08 +0400
Subject: [PATCH v4] Make amcheck checking UNIQUE constraint for btree index.
 On index with unique constraint ake check that only one table entry for the
 equal keys (including all posting list entries) is visible. Report error if
 not and show all index entries violating the constraint under warning level.

Authors: Anastasia Lubennikova <a.lubennikova@postgrespro.ru>, Pavel Borisov <pashkin.elfe@gmail.com>
---
 contrib/amcheck/Makefile                   |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql      |  28 ++
 contrib/amcheck/amcheck.control            |   2 +-
 contrib/amcheck/expected/check_btree.out   | 146 +++++++++
 contrib/amcheck/expected/check_btree_1.out | 333 +++++++++++++++++++++
 contrib/amcheck/sql/check_btree.sql        |  27 ++
 contrib/amcheck/verify_nbtree.c            | 311 ++++++++++++++++++-
 7 files changed, 832 insertions(+), 17 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/expected/check_btree_1.out

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..f76e6941ffe
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,28 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded version of the 1.3 function signature
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..b191a4df862 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,157 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+-- Check unique index with no uniqueness check. Should not complain.
+SELECT bt_index_check('bttest_unique_idx', true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- Check unique indes with uniquensee check. Should detect constraint violation cases.
+SELECT bt_index_check('bttest_unique_idx', true, true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 2 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and posting 4 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 4 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 2 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and posting 3 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 3 and posting 4 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 4 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness may be violated for index "bttest_unique_idx": Index tid=(5,1) doesn't have visible heap tids and key is equal to the tid=(4,6) posting 4 (points to heap tid=(9,2)). Cross-page unique constraint violation can be missed. Vacuum the table and repeat the check.
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,5) and tid=(5,6) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true, true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 1 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 1 and posting 2 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and tid=(1,3) posting 0 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and tid=(1,4) posting 0 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and tid=(1,5) posting 0 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and tid=(1,6) posting 0 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and tid=(2,2) posting 0 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 0 and posting 1 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 1 and posting 2 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(2,2) posting 2 and tid=(2,3) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and tid=(4,3) posting 0 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and tid=(4,4) posting 0 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and tid=(4,5) posting 0 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) posting 0 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 0 and posting 1 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 1 and posting 2 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 2 and posting 3 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 3 and posting 4 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,6) posting 4 and tid=(5,1) (point to heap tid=(9,2) and tid=(9,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,1) and tid=(5,2) (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,2) and tid=(5,3) (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,3) and tid=(5,4) (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(5,4) and tid=(5,5) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/expected/check_btree_1.out b/contrib/amcheck/expected/check_btree_1.out
new file mode 100644
index 00000000000..6aa66e42118
--- /dev/null
+++ b/contrib/amcheck/expected/check_btree_1.out
@@ -0,0 +1,333 @@
+CREATE TABLE bttest_a(id int8);
+CREATE TABLE bttest_b(id int8);
+CREATE TABLE bttest_multi(id int8, data int8);
+CREATE TABLE delete_test_table (a bigint, b bigint, c bigint, d bigint);
+-- Stabalize tests
+ALTER TABLE bttest_a SET (autovacuum_enabled = false);
+ALTER TABLE bttest_b SET (autovacuum_enabled = false);
+ALTER TABLE bttest_multi SET (autovacuum_enabled = false);
+ALTER TABLE delete_test_table SET (autovacuum_enabled = false);
+INSERT INTO bttest_a SELECT * FROM generate_series(1, 100000);
+INSERT INTO bttest_b SELECT * FROM generate_series(100000, 1, -1);
+INSERT INTO bttest_multi SELECT i, i%2  FROM generate_series(1, 100000) as i;
+CREATE INDEX bttest_a_idx ON bttest_a USING btree (id) WITH (deduplicate_items = ON);
+CREATE INDEX bttest_b_idx ON bttest_b USING btree (id);
+CREATE UNIQUE INDEX bttest_multi_idx ON bttest_multi
+USING btree (id) INCLUDE (data);
+CREATE ROLE regress_bttest_role;
+-- verify permissions are checked (error due to function not callable)
+SET ROLE regress_bttest_role;
+SELECT bt_index_check('bttest_a_idx'::regclass);
+ERROR:  permission denied for function bt_index_check
+SELECT bt_index_parent_check('bttest_a_idx'::regclass);
+ERROR:  permission denied for function bt_index_parent_check
+RESET ROLE;
+-- we, intentionally, don't check relation permissions - it's useful
+-- to run this cluster-wide with a restricted account, and as tested
+-- above explicit permission has to be granted for that.
+GRANT EXECUTE ON FUNCTION bt_index_check(regclass) TO regress_bttest_role;
+GRANT EXECUTE ON FUNCTION bt_index_parent_check(regclass) TO regress_bttest_role;
+GRANT EXECUTE ON FUNCTION bt_index_check(regclass, boolean) TO regress_bttest_role;
+GRANT EXECUTE ON FUNCTION bt_index_parent_check(regclass, boolean) TO regress_bttest_role;
+SET ROLE regress_bttest_role;
+SELECT bt_index_check('bttest_a_idx');
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx');
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+RESET ROLE;
+-- verify plain tables are rejected (error)
+SELECT bt_index_check('bttest_a');
+ERROR:  "bttest_a" is not an index
+SELECT bt_index_parent_check('bttest_a');
+ERROR:  "bttest_a" is not an index
+-- verify non-existing indexes are rejected (error)
+SELECT bt_index_check(17);
+ERROR:  could not open relation with OID 17
+SELECT bt_index_parent_check(17);
+ERROR:  could not open relation with OID 17
+-- verify wrong index types are rejected (error)
+BEGIN;
+CREATE INDEX bttest_a_brin_idx ON bttest_a USING brin(id);
+SELECT bt_index_parent_check('bttest_a_brin_idx');
+ERROR:  only B-Tree indexes are supported as targets for verification
+DETAIL:  Relation "bttest_a_brin_idx" is not a B-Tree index.
+ROLLBACK;
+-- normal check outside of xact
+SELECT bt_index_check('bttest_a_idx');
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- more expansive tests
+SELECT bt_index_check('bttest_a_idx', true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+BEGIN;
+SELECT bt_index_check('bttest_a_idx');
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx');
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- make sure we don't have any leftover locks
+SELECT * FROM pg_locks
+WHERE relation = ANY(ARRAY['bttest_a', 'bttest_a_idx', 'bttest_b', 'bttest_b_idx']::regclass[])
+    AND pid = pg_backend_pid();
+ locktype | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction | pid | mode | granted | fastpath | waitstart 
+----------+----------+----------+------+-------+------------+---------------+---------+-------+----------+--------------------+-----+------+---------+----------+-----------
+(0 rows)
+
+COMMIT;
+-- Deduplication
+TRUNCATE bttest_a;
+INSERT INTO bttest_a SELECT 42 FROM generate_series(1, 2000);
+SELECT bt_index_check('bttest_a_idx', true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- normal check outside of xact for index with included columns
+SELECT bt_index_check('bttest_multi_idx');
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- more expansive tests for index with included columns
+SELECT bt_index_parent_check('bttest_multi_idx', true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- repeat expansive tests for index built using insertions
+TRUNCATE bttest_multi;
+INSERT INTO bttest_multi SELECT i, i%2  FROM generate_series(1, 100000) as i;
+SELECT bt_index_parent_check('bttest_multi_idx', true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+--
+-- Test for multilevel page deletion/downlink present checks, and rootdescend
+-- checks
+--
+INSERT INTO delete_test_table SELECT i, 1, 2, 3 FROM generate_series(1,80000) i;
+ALTER TABLE delete_test_table ADD PRIMARY KEY (a,b,c,d);
+-- Delete most entries, and vacuum, deleting internal pages and creating "fast
+-- root"
+DELETE FROM delete_test_table WHERE a < 79990;
+VACUUM delete_test_table;
+SELECT bt_index_parent_check('delete_test_table_pkey', true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+--
+-- BUG #15597: must not assume consistent input toasting state when forming
+-- tuple.  Bloom filter must fingerprint normalized index tuple representation.
+--
+CREATE TABLE toast_bug(buggy text);
+ALTER TABLE toast_bug ALTER COLUMN buggy SET STORAGE extended;
+CREATE INDEX toasty ON toast_bug(buggy);
+-- pg_attribute entry for toasty.buggy (the index) will have plain storage:
+UPDATE pg_attribute SET attstorage = 'p'
+WHERE attrelid = 'toasty'::regclass AND attname = 'buggy';
+-- Whereas pg_attribute entry for toast_bug.buggy (the table) still has extended storage:
+SELECT attstorage FROM pg_attribute
+WHERE attrelid = 'toast_bug'::regclass AND attname = 'buggy';
+ attstorage 
+------------
+ x
+(1 row)
+
+-- Insert compressible heap tuple (comfortably exceeds TOAST_TUPLE_THRESHOLD):
+INSERT INTO toast_bug SELECT repeat('a', 2200);
+-- Should not get false positive report of corruption:
+SELECT bt_index_check('toasty', true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+-- Check unique index with no uniqueness check. Should not complain.
+SELECT bt_index_check('bttest_unique_idx', true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+-- Check unique indes with uniquensee check. Should detect constraint violation cases.
+SELECT bt_index_check('bttest_unique_idx', true, true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 2 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and posting 4 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 4 and posting 5 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 5 and tid=(1,3) posting 0 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and posting 5 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 5 and tid=(1,4) posting 0 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and posting 5 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 5 and tid=(1,5) posting 0 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and posting 5 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 5 and tid=(1,6) posting 0 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 3 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and posting 4 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 4 and posting 5 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 5 and tid=(2,2) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 4 and posting 5 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 5 and tid=(4,2) posting 0 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and posting 5 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 5 and tid=(4,3) posting 0 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and posting 5 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 5 and tid=(4,4) posting 0 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and posting 5 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 5 and tid=(4,5) posting 0 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 2 (point to heap tid=(9,2) and tid=(9,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and posting 5 (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 5 and tid=(4,6) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true, true);
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 0 and posting 1 (point to heap tid=(0,1) and tid=(0,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 1 and posting 2 (point to heap tid=(0,3) and tid=(0,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 2 and posting 3 (point to heap tid=(0,4) and tid=(0,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 3 and posting 4 (point to heap tid=(0,5) and tid=(0,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,2) posting 4 and tid=(1,3) posting 0 (point to heap tid=(0,6) and tid=(1,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 0 and posting 1 (point to heap tid=(1,1) and tid=(1,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 1 and posting 2 (point to heap tid=(1,2) and tid=(1,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 2 and posting 3 (point to heap tid=(1,3) and tid=(1,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 3 and posting 4 (point to heap tid=(1,4) and tid=(1,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 4 and posting 5 (point to heap tid=(1,5) and tid=(1,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,3) posting 5 and tid=(1,4) posting 0 (point to heap tid=(1,6) and tid=(2,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 0 and posting 1 (point to heap tid=(2,1) and tid=(2,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 1 and posting 2 (point to heap tid=(2,2) and tid=(2,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 2 and posting 3 (point to heap tid=(2,3) and tid=(2,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 3 and posting 4 (point to heap tid=(2,4) and tid=(2,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 4 and posting 5 (point to heap tid=(2,5) and tid=(2,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,4) posting 5 and tid=(1,5) posting 0 (point to heap tid=(2,6) and tid=(3,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 0 and posting 1 (point to heap tid=(3,1) and tid=(3,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 1 and posting 2 (point to heap tid=(3,2) and tid=(3,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 2 and posting 3 (point to heap tid=(3,3) and tid=(3,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 3 and posting 4 (point to heap tid=(3,4) and tid=(3,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 4 and posting 5 (point to heap tid=(3,5) and tid=(3,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,5) posting 5 and tid=(1,6) posting 0 (point to heap tid=(3,6) and tid=(4,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 0 and posting 1 (point to heap tid=(4,1) and tid=(4,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 1 and posting 2 (point to heap tid=(4,4) and tid=(4,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 2 and posting 3 (point to heap tid=(4,5) and tid=(4,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(1,6) posting 3 and tid=(2,2) (point to heap tid=(4,6) and tid=(5,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 0 and posting 1 (point to heap tid=(5,2) and tid=(5,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 1 and posting 2 (point to heap tid=(5,3) and tid=(5,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 2 and posting 3 (point to heap tid=(5,4) and tid=(5,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 3 and posting 4 (point to heap tid=(5,5) and tid=(5,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 4 and posting 5 (point to heap tid=(5,6) and tid=(6,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,1) posting 5 and tid=(4,2) posting 0 (point to heap tid=(6,1) and tid=(6,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 0 and posting 1 (point to heap tid=(6,2) and tid=(6,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 1 and posting 2 (point to heap tid=(6,3) and tid=(6,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 2 and posting 3 (point to heap tid=(6,4) and tid=(6,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 3 and posting 4 (point to heap tid=(6,5) and tid=(6,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 4 and posting 5 (point to heap tid=(6,6) and tid=(7,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,2) posting 5 and tid=(4,3) posting 0 (point to heap tid=(7,1) and tid=(7,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 0 and posting 1 (point to heap tid=(7,2) and tid=(7,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 1 and posting 2 (point to heap tid=(7,3) and tid=(7,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 2 and posting 3 (point to heap tid=(7,4) and tid=(7,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 3 and posting 4 (point to heap tid=(7,5) and tid=(7,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 4 and posting 5 (point to heap tid=(7,6) and tid=(8,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,3) posting 5 and tid=(4,4) posting 0 (point to heap tid=(8,1) and tid=(8,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 0 and posting 1 (point to heap tid=(8,2) and tid=(8,3)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 1 and posting 2 (point to heap tid=(8,3) and tid=(8,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 2 and posting 3 (point to heap tid=(8,4) and tid=(8,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 3 and posting 4 (point to heap tid=(8,5) and tid=(8,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 4 and posting 5 (point to heap tid=(8,6) and tid=(9,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,4) posting 5 and tid=(4,5) posting 0 (point to heap tid=(9,1) and tid=(9,2)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 0 and posting 1 (point to heap tid=(9,2) and tid=(9,4)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 1 and posting 2 (point to heap tid=(9,4) and tid=(9,5)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 2 and posting 3 (point to heap tid=(9,5) and tid=(9,6)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 3 and posting 4 (point to heap tid=(9,6) and tid=(10,1)).
+WARNING:  index uniqueness is violated for index "bttest_unique_idx": Index tid=(4,5) posting 4 and tid=(4,6) (point to heap tid=(10,1) and tid=(10,2)).
+ERROR:  index "bttest_unique_idx" is corrupted. There are tuples violating UNIQUE constraint
+DETAIL:  Details are in the previous log messages under WARNING priority
+-- cleanup
+DROP TABLE bttest_a;
+DROP TABLE bttest_b;
+DROP TABLE bttest_multi;
+DROP TABLE delete_test_table;
+DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
+DROP OWNED BY regress_bttest_role; -- permissions
+DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..dbe8b48afc7 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,38 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+CREATE TABLE bttest_unique(a varchar(50), b varchar(1500), c bytea, d varchar(50));
+CREATE UNIQUE INDEX bttest_unique_idx ON bttest_unique(a,b);
+UPDATE pg_catalog.pg_index SET indisunique = false
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+INSERT INTO bttest_unique
+	SELECT 	i::text::varchar,
+			array_to_string(array(
+				SELECT substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ((random()*(36-1)+1)::integer), 1)
+			FROM generate_series(1,1300)),'')::varchar,
+	i::text::bytea, i::text::varchar
+	FROM generate_series(0,1) AS i, generate_series(0,30) AS x;
+UPDATE pg_catalog.pg_index SET indisunique = true
+WHERE indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'bttest_unique');
+
+DELETE FROM bttest_unique WHERE ctid::text='(0,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,2)';
+DELETE FROM bttest_unique WHERE ctid::text='(4,3)';
+DELETE FROM bttest_unique WHERE ctid::text='(9,3)';
+-- Check unique index with no uniqueness check. Should not complain.
+SELECT bt_index_check('bttest_unique_idx', true);
+-- Check unique indes with uniquensee check. Should detect constraint violation cases.
+SELECT bt_index_check('bttest_unique_idx', true, true);
+VACUUM bttest_unique;
+SELECT bt_index_check('bttest_unique_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index c4ca6339182..68644fa3285 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -78,11 +78,20 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool 		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking.
+	 * Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -137,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -187,9 +210,10 @@ static ItemId PageGetItemIdCareful(BtreeCheckState *state, BlockNumber block,
 static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 													  IndexTuple itup, bool nonpivot);
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
+static bool errflag; /* Output ERROR at the end of amcheck */
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -202,17 +226,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool        checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -226,13 +253,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -242,7 +272,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +353,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -417,7 +447,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -449,6 +480,18 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
+	/*
+	 * We need a snapshot it to check uniqueness of the index
+	 * For better performance, take it once per index check.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+			state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+	}
 
 	if (state->heapallindexed)
 	{
@@ -632,7 +675,16 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
+
+	if (errflag)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDEX_CORRUPTED),
+				errmsg("index \"%s\" is corrupted. There are tuples violating UNIQUE constraint",
+						RelationGetRelationName(state->rel)),
+				errdetail_internal("Details are in the previous log messages under WARNING priority")));
 }
 
 /*
@@ -1006,6 +1058,149 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 								btpo_prev_from_target)));
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+							  tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void bt_report_duplicate(BtreeCheckState *state,
+				 ItemPointer tid, BlockNumber block, OffsetNumber offset,
+				 int posting,
+				 ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+				 int nposting)
+{
+	char	   	*htid,
+				*nhtid,
+				*itid,
+				*nitid = "",
+				*pposting = "",
+				*pnposting = "";
+
+	errflag = true;
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(nexttid),
+					ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s).",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid)));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+		BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+		OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool has_visible_entry = false;
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid (*lVis_tid))
+				{
+					bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, i);
+				}
+				/*
+				 * Prevent double reporting unique violation between the posting
+				 * list entries of a first tuple on the page after cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid (*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list.
+	 * If TID is visible, save info about it for next comparisons in the loop in
+	 * bt_page_check(). If also lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid (*lVis_tid))
+			{
+				bt_report_duplicate(state,
+											*lVis_tid, *lVis_block,
+											*lVis_offset, *lVis_i,
+											tid, targetblock,
+											offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+									   *lVis_block != targetblock)
+		ereport(WARNING,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			errmsg("index uniqueness may be violated for index \"%s\": "
+					"Index tid=(%u,%u) doesn't have visible heap tids and key "
+					"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+					"Cross-page unique constraint violation can be missed. "
+					"Vacuum the table and repeat the check.",
+					RelationGetRelationName(state->rel),
+					targetblock, offset,
+					*lVis_block, *lVis_offset, psprintf(" posting %u", *lVis_i),
+					ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+					ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+}
+
 /*
  * Function performs the following checks on target page, or pages ancillary to
  * target page:
@@ -1026,6 +1221,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1047,6 +1245,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber offset;
 	OffsetNumber max;
 	BTPageOpaque topaque;
+	/* last visible entry info for checking indexes with unique constraint */
+	int			 lVis_i = -1; /* the position of last visible item for posting
+							   * tuple. for non-posting tuple (-1)
+							   */
+	ItemPointer	 lVis_tid = NULL;
+	BlockNumber	 lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
 
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1438,6 +1643,39 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking
+		 * heap tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+					&lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+				 OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+			/* Invalidate scankey tid to make _bt_compare compare only keys
+			 * in the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+						OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid; /* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1455,12 +1693,14 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1494,6 +1734,44 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one found
+			 * equal items is visible.
+			 */
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+					rightkey && P_ISLEAF(topaque))
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  state->targetblock + 1);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+							break;
+
+					itemid = PageGetItemIdCareful(state, state->targetblock + 1,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, state->targetblock + 1, rightfirstoffset,
+									&lVis_i, &lVis_tid, &lVis_offset,
+									&lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1539,9 +1817,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1704,6 +1984,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
-- 
2.28.0

In reply to: Mark Dilger (#7)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, Mar 1, 2021 at 11:22 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

If bt_index_check() and bt_index_parent_check() are to have this functionality, shouldn't there be an option controlling it much as the option (heapallindexed boolean) controls checking whether all entries in the heap are indexed in the btree? It seems inconsistent to have an option to avoid checking the heap for that, but not for this.

I agree. Actually, it should probably use the same snapshot as the
heapallindexed=true case. So either only perform unique constraint
verification when that option is used, or invent a new option that
will still share the snapshot used by heapallindexed=true (when the
options are combined).

The regression test you provided is not portable. I am getting lots of errors due to differing output of the form "page lsn=0/4DAD7E0". You might turn this into a TAP test and use a regular expression to check the output.

I would test this using a custom opclass that does simple fault
injection. For example, an opclass that indexes integers, but can be
configured to dynamically make 0 values equal or unequal to each
other. That's more representative of real-world problems.

You "break the warranty" by updating pg_index, even compared to
updating other system catalogs. In particular, you break the
"indcheckxmin wait -- wait for xmin to be old before using index"
stuff in get_relation_info(). So it seems worse than updating
pg_attribute, for example (which is something that the tests do
already).

--
Peter Geoghegan

In reply to: Pavel Borisov (#1)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, Feb 8, 2021 at 2:46 AM Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Caveat: if the first entry on the next index page has a key equal to the key on a previous page AND all heap tid's corresponding to this entry are invisible, currently cross-page check can not detect unique constraint violation between previous index page entry and 2nd, 3d and next current index page entries. In this case, there would be a message that recommends doing VACUUM to remove the invisible entries from the index and repeat the check. (Generally, it is recommended to do vacuum before the check, but for the testing purpose I'd recommend turning it off to check the detection of visible-invisible-visible duplicates scenarios)

It's rather unlikely that equal values in a unique index will end up
on different leaf pages. It's really rare, in fact. This following
comment block from nbtinsert.c (which appears right before we call
_bt_check_unique()) explains why this is:

* It might be necessary to check a page to the right in _bt_check_unique,
* though that should be very rare. In practice the first page the value ...

You're going to have to "couple" buffer locks in the style of
_bt_check_unique() (as well as keeping a buffer lock on "the first
leaf page a duplicate might be on" throughout) if you need the test to
work reliably. But why bother with that? The tool doesn't have to be
100% perfect at detecting corruption (nothing can be), and it's rather
unlikely that it will matter for this test. A simple test that doesn't
handle cross-page duplicates is still going to be very effective.

I don't think that it's acceptable for your new check to raise a
WARNING instead of an ERROR. I especially don't like that the new
unique checking functionality merely warns that the index *might* be
corrupt. False positives are always unacceptable within amcheck, and I
think that this is a false positive.

--
Peter Geoghegan

#16Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Peter Geoghegan (#15)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

You're going to have to "couple" buffer locks in the style of
_bt_check_unique() (as well as keeping a buffer lock on "the first
leaf page a duplicate might be on" throughout) if you need the test to
work reliably. But why bother with that? The tool doesn't have to be
100% perfect at detecting corruption (nothing can be), and it's rather
unlikely that it will matter for this test. A simple test that doesn't
handle cross-page duplicates is still going to be very effective.

Indeed at first, I did the test which doesn't bother checking duplicates
cross-page which I considered very rare, but then a customer sent me his
corrupted index where I found this rare thing which was not detectable by
amcheck and he was puzzled with the issue. Even rare inconsistencies can
appear when people handle huge amounts of data. So I did an update that
handles a wider class of errors. I don't suppose that cross page unique
check is expensive as it uses same things that are already used in amcheck
for cross-page checks.

Is it suitable if I omit suspected duplicates message in the very-very rare
case amcheck can not detect but leave cross-page checks?

I don't think that it's acceptable for your new check to raise a
WARNING instead of an ERROR.

It is not instead of an ERROR. If at least one violation is detected,
amcheck will output the final ERROR message. The purpose is not to stop
checking at the first violation. But I can make them reported in a current
amcheck style if it is necessary.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

#17Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#13)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mar 2, 2021, at 6:08 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

I completely agree that checking uniqueness requires looking at the heap, but I don't agree that every caller of bt_index_check on an index wants that particular check to be performed. There are multiple ways in which an index might be corrupt, and Peter wrote the code to only check some of them by default, with options to expand the checks to other things. This is why heapallindexed is optional. If you don't want to pay the price of checking all entries in the heap against the btree, you don't have to.

I've got the idea and revised the patch accordingly. Thanks!
Pfa v4 of a patch. I've added an optional argument to allow uniqueness checks for the unique indexes.
Also, I added a test variant to make them work on 32-bit systems. Unfortunately, converting the regression test to TAP would be a pain for me. Hope it can be used now as a 2-variant regression test for 32 and 64 bit systems.

Thank you for your consideration!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com
<v4-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patch>

Looking over v4, here are my review comments...

I created the file contrib/amcheck/amcheck--1.2--1.3.sql during the v14 development cycle, so that is not released yet. If your patch goes out in v14, does it need to create an amcheck--1.3--1.4.sql, or could you fit your changes into the 1.2--1.3.sql file? (Does the project have a convention governing this?) This is purely a question. I'm not advising you to change anything here.

You need to update doc/src/sgml/amcheck.sgml to reflect the changes you made to the bt_index_check and bt_index_parent_check functions.

You need to update the recently committed src/bin/pg_amcheck project to include --checkunique as an option. This client application already has flags for heapallindexed and rootdescend. I can help with that if it isn't obvious what needs to be done. Note that pg_amcheck/t contains TAP tests that exercise the options, so you'll need to extend code coverage to include this new option.

On Mar 2, 2021, at 7:10 PM, Peter Geoghegan <pg@bowt.ie> wrote:

I don't think that it's acceptable for your new check to raise a
WARNING instead of an ERROR.

You already responded to Peter, and I can see that after raising WARNINGs about an index, the code raises an ERROR. That is different from behavior that pg_amcheck currently expects from contrib/amcheck functions. It will be interesting to see if that makes integration harder.

On Mar 2, 2021, at 6:54 PM, Peter Geoghegan <pg@bowt.ie> wrote:

The regression test you provided is not portable. I am getting lots of errors due to differing output of the form "page lsn=0/4DAD7E0". You might turn this into a TAP test and use a regular expression to check the output.

I would test this using a custom opclass that does simple fault
injection. For example, an opclass that indexes integers, but can be
configured to dynamically make 0 values equal or unequal to each
other. That's more representative of real-world problems.

You "break the warranty" by updating pg_index, even compared to
updating other system catalogs. In particular, you break the
"indcheckxmin wait -- wait for xmin to be old before using index"
stuff in get_relation_info(). So it seems worse than updating
pg_attribute, for example (which is something that the tests do
already).

Take a look at src/bin/pg_amcheck/t/005_opclass_damage.pl for an example of changing an opclass to test btree index breakage.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#18David Steele
david@pgmasters.net
In reply to: Mark Dilger (#17)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On 3/15/21 11:11 AM, Mark Dilger wrote:

On Mar 2, 2021, at 6:08 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

I completely agree that checking uniqueness requires looking at the heap, but I don't agree that every caller of bt_index_check on an index wants that particular check to be performed. There are multiple ways in which an index might be corrupt, and Peter wrote the code to only check some of them by default, with options to expand the checks to other things. This is why heapallindexed is optional. If you don't want to pay the price of checking all entries in the heap against the btree, you don't have to.

I've got the idea and revised the patch accordingly. Thanks!
Pfa v4 of a patch. I've added an optional argument to allow uniqueness checks for the unique indexes.
Also, I added a test variant to make them work on 32-bit systems. Unfortunately, converting the regression test to TAP would be a pain for me. Hope it can be used now as a 2-variant regression test for 32 and 64 bit systems.

Thank you for your consideration!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com
<v4-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patch>

Looking over v4, here are my review comments...

This patch appears to need some work and has not been updated in several
weeks, so marking Returned with Feedback.

Please submit to the next CF when you have a new patch.

Regards,
--
-David
david@pgmasters.net

#19Pavel Borisov
pashkin.elfe@gmail.com
In reply to: David Steele (#18)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

I completely agree that checking uniqueness requires looking at the

heap, but I don't agree that every caller of bt_index_check on an index
wants that particular check to be performed. There are multiple ways in
which an index might be corrupt, and Peter wrote the code to only check
some of them by default, with options to expand the checks to other
things. This is why heapallindexed is optional. If you don't want to pay
the price of checking all entries in the heap against the btree, you don't
have to.

I've got the idea and revised the patch accordingly. Thanks!
Pfa v4 of a patch. I've added an optional argument to allow uniqueness

checks for the unique indexes.

Also, I added a test variant to make them work on 32-bit systems.

Unfortunately, converting the regression test to TAP would be a pain for
me. Hope it can be used now as a 2-variant regression test for 32 and 64
bit systems.

Thank you for your consideration!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com
<v4-0001-Make-amcheck-checking-UNIQUE-constraint-for-btree.patch>

Looking over v4, here are my review comments...

Mark and Peter, big thanks for your ideas!

I had little time to work on this feature until recently, but finally, I've
elaborated v5 patch (PFA)
It contains the following improvements, most of which are based on your
consideration:

- Amcheck tests are reworked into TAP-tests with "break the warranty" by
comparison function changes in the opclass instead of pg_index update.
Mark, again thanks for the sample!
- Added new --checkunique option into pg_amcheck.
- Added documentation and tests for amcheck and pg_amcheck new functions.
- Results are output at ERROR log level like it is done in the other
amcheck tests.
- Rare case of inability to check due to the first entry on a leaf page
being both: (1) equal to the last one on the previous page and (2) deleted
in the heap, is demoted to DEBUG1 log level. In this, I folowed Peter's
consideration that amcheck should do its best to check, but can not always
verify everything. The case is expected to be extremely rare.
- Made pages connectivity based on btpo_next (corrected a bug in the code,
I found meanwhile)
- If snapshot is already taken in heapallindexed case, reuse it for unique
constraint check.

The patch is pgindented and rebased on the current PG master code.
I'd like to re-attach the patch v5 to the upcoming CF if you don't mind.

I also want to add that some customers face index uniqueness
violations more often than I've expected, and I hope this check could also
help some other PostgreSQL customers.

Your further considerations are welcome as always!
--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v5-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchapplication/octet-stream; name=v5-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchDownload
From b9bdb2dde7d97e09e9a95daf43c8c843ff3876d8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 20 Dec 2021 16:57:03 +0400
Subject: [PATCH v5] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <a.lubennikova@postgrespro.ru>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <m.orlov@postgrespro.ru>
---
 contrib/amcheck/Makefile                   |   2 +-
 contrib/amcheck/amcheck.control            |   2 +-
 contrib/amcheck/expected/check_btree.out   |  27 ++
 contrib/amcheck/sql/check_btree.sql        |   7 +
 contrib/amcheck/verify_nbtree.c            | 327 ++++++++++++++++++++-
 doc/src/sgml/amcheck.sgml                  |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml           |  11 +
 src/bin/pg_amcheck/pg_amcheck.c            |  17 +-
 src/bin/pg_amcheck/t/005_opclass_damage.pl |  75 ++++-
 9 files changed, 457 insertions(+), 25 deletions(-)

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..47250ec2f0f 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,38 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
+ERROR:  table "bttest_unique" does not exist
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..1acee3a2439 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,18 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d3b29d3d890..80ddf3c819b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,41 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1707,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1750,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1834,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2005,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index d4a53c8e636..7089ed6459f 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -267,6 +269,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +452,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -871,7 +877,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s, "
+						  "checkunique := %s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +887,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (opts.checkunique ? "true" : "false"),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s,"
+						  "checkunique := %s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +901,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (opts.checkunique ? "true" : "false"),
 						  rel->reloid);
 }
 
@@ -1187,6 +1197,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index 2f86f4f2a40..78ecb7c6321 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -8,7 +8,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 5;
+use Test::More tests => 10;
 
 my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init;
@@ -22,6 +22,17 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
@@ -30,6 +41,21 @@ $node->safe_psql(
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+
+	CREATE OPERATOR CLASS int4_custom_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
+
+	CREATE TABLE bttest_unique (i int4);
+	INSERT INTO bttest_unique
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON bttest_unique
+						USING btree (i int4_custom_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,3 +83,50 @@ $node->command_checks_all(
 	[],
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
+
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
-- 
2.24.3 (Apple Git-128)

#20Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#19)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Dec 20, 2021, at 7:37 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

The patch is pgindented and rebased on the current PG master code.

Thank you, Pavel.

The tests in check_btree.sql no longer create a bttest_unique table, so the DROP TABLE is surplusage:

+DROP TABLE bttest_unique;
+ERROR:  table "bttest_unique" does not exist

The changes in pg_amcheck.c to pass the new checkunique parameter will likely need to be based on a amcheck version check. The implementation of prepare_btree_command() in pg_amcheck.c should be kept compatible with older versions of amcheck, because it connects to remote servers and you can't know in advance that the remote servers are as up-to-date as the machine where pg_amcheck is installed. I'm thinking specifically about this change:

@@ -871,7 +877,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
    if (opts.parent_check)
        appendPQExpBuffer(sql,
                          "SELECT %s.bt_index_parent_check("
-                         "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+                         "index := c.oid, heapallindexed := %s, rootdescend := %s, "
+                         "checkunique := %s)"
                          "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
                          "WHERE c.oid = %u "
                          "AND c.oid = i.indexrelid "

If the user calls pg_amcheck with --checkunique, and one or more remote servers have an amcheck version < 1.4, at a minimum you'll need to avoid calling bt_index_parent_check with that parameter, and probably also you'll either need to raise a warning or perhaps an error telling the user that such a check cannot be performed.

You've forgotten to include contrib/amcheck/amcheck--1.3--1.4.sql in the v5 patch, resulting in a failed install:

/usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql ./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql '/Users/mark.dilger/hydra/unique_review.5/tmp_install/Users/mark.dilger/pgtest/test_install/share/postgresql/extension/'
install: ./amcheck--1.3--1.4.sql: No such file or directory
make[2]: *** [install] Error 71
make[1]: *** [checkprep] Error 2

Using the one from the v4 patch fixes the problem. Please include this file in v6, to simplify the review process.

The changes to t/005_opclass_damage.pl look ok. The creation of a new table for the new test seems unnecessary, but only problematic in that it makes the test slightly longer to read. I recommend changing the test to use the same table that the prior test uses, but that is just a recommendation, not a requirement.

You should add coverage for --checkunique to t/003_check.pl.

You should add coverage for multiple PostgreSQL::Test::Cluster instances running differing versions of amcheck, perhaps some on version 1.3 and some on version 1.4. Then test that the --checkunique option works adequately.

I have not reviewed the changes to verify_nbtree.c. I'll leave that to Peter.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#21Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#20)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

The tests in check_btree.sql no longer create a bttest_unique table, so
the DROP TABLE is surplusage:

+DROP TABLE bttest_unique;
+ERROR:  table "bttest_unique" does not exist

The changes in pg_amcheck.c to pass the new checkunique parameter will
likely need to be based on a amcheck version check. The implementation of
prepare_btree_command() in pg_amcheck.c should be kept compatible with
older versions of amcheck, because it connects to remote servers and you
can't know in advance that the remote servers are as up-to-date as the
machine where pg_amcheck is installed. I'm thinking specifically about
this change:

@@ -871,7 +877,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo
*rel, PGconn *conn)
if (opts.parent_check)
appendPQExpBuffer(sql,
"SELECT %s.bt_index_parent_check("
-                         "index := c.oid, heapallindexed := %s,
rootdescend := %s)"
+                         "index := c.oid, heapallindexed := %s,
rootdescend := %s, "
+                         "checkunique := %s)"
"\nFROM pg_catalog.pg_class c,
pg_catalog.pg_index i "
"WHERE c.oid = %u "
"AND c.oid = i.indexrelid "

If the user calls pg_amcheck with --checkunique, and one or more remote
servers have an amcheck version < 1.4, at a minimum you'll need to avoid
calling bt_index_parent_check with that parameter, and probably also you'll
either need to raise a warning or perhaps an error telling the user that
such a check cannot be performed.

You've forgotten to include contrib/amcheck/amcheck--1.3--1.4.sql in the
v5 patch, resulting in a failed install:

/usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql
./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql
'/Users/mark.dilger/hydra/unique_review.5/tmp_install/Users/mark.dilger/pgtest/test_install/share/postgresql/extension/'
install: ./amcheck--1.3--1.4.sql: No such file or directory
make[2]: *** [install] Error 71
make[1]: *** [checkprep] Error 2

Using the one from the v4 patch fixes the problem. Please include this
file in v6, to simplify the review process.

The changes to t/005_opclass_damage.pl look ok. The creation of a new
table for the new test seems unnecessary, but only problematic in that it
makes the test slightly longer to read. I recommend changing the test to
use the same table that the prior test uses, but that is just a
recommendation, not a requirement.

You should add coverage for --checkunique to t/003_check.pl.

You should add coverage for multiple PostgreSQL::Test::Cluster instances
running differing versions of amcheck, perhaps some on version 1.3 and some
on version 1.4. Then test that the --checkunique option works adequately.

Thank you, Mark!

In v6 (PFA) I've made the changes on your advice i.e.

- pg_amcheck with --checkunique option will ignore uniqueness check (with a
warning) if amcheck version in a db is <1.4 and doesn't support the feature.
- fixed unnecessary drop table in regression
- use the existing table for uniqueness check in 005_opclass_damage.pl
- added tests into 003_check.pl . It is only smoke test that just verifies
new functions.
- added test contrib/amcheck/t/004_verify_nbtree_unique.pl it is more
extensive test based on opclass damage which was intended to be main test
for amcheck, but which I've forgotten to add to commit in v5.
005_opclass_damage.pl test, which you've seen in v5 is largely based on
first part of 004_verify_nbtree_unique.pl (with the later calling
pg_amcheck, and the former calling bt_index_check(),
bt_index_parent_check() )
- added forgotten upgrade script amcheck--1.3--1.4.sql (from v4)

You are welcome with more considerations.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v6-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchapplication/octet-stream; name=v6-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchDownload
From cf2d11a2e359d173bb92f6345a7f060630c03d64 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 22 Dec 2021 11:39:15 +0400
Subject: [PATCH v6] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <m.orlov@postgrespro.ru>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  25 ++
 contrib/amcheck/sql/check_btree.sql           |   6 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 327 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  56 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  48 ++-
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  75 +++-
 12 files changed, 798 insertions(+), 31 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..0144767b36e 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,6 +177,31 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..4eb5ffb21d3 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,6 +115,12 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d3b29d3d890..80ddf3c819b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,41 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1707,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1750,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1834,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2005,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index d4a53c8e636..30914db55b2 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,34 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		if (opts.checkunique == true)
+		{
+			dat->is_checkunique = true;
+
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = pstrdup(PQgetvalue(result, 0, 1));
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +906,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +916,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +930,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1139,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1226,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 5913fcc5305..d3f0259bcbd 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -8,7 +8,7 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 
 use Fcntl qw(:seek);
-use Test::More tests => 63;
+use Test::More tests => 75;
 
 my ($node, $port, %corrupt_page, %remove_relation);
 
@@ -258,6 +258,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,3 +520,46 @@ $node->command_checks_all(
 	[ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
\ No newline at end of file
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index 2f86f4f2a40..78ecb7c6321 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -8,7 +8,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 5;
+use Test::More tests => 10;
 
 my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init;
@@ -22,6 +22,17 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
@@ -30,6 +41,21 @@ $node->safe_psql(
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+
+	CREATE OPERATOR CLASS int4_custom_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
+
+	CREATE TABLE bttest_unique (i int4);
+	INSERT INTO bttest_unique
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON bttest_unique
+						USING btree (i int4_custom_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,3 +83,50 @@ $node->command_checks_all(
 	[],
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
+
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
-- 
2.24.3 (Apple Git-128)

#22Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#21)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Dec 22, 2021, at 12:01 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Thank you, Mark!

In v6 (PFA) I've made the changes on your advice i.e.

- pg_amcheck with --checkunique option will ignore uniqueness check (with a warning) if amcheck version in a db is <1.4 and doesn't support the feature.

Ok.

+           int         vmaj = 0,
+                       vmin = 0,
+                       vrev = 0;
+           const char *amcheck_version = pstrdup(PQgetvalue(result, 0, 1));
+
+           sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);

The pstrdup is unnecessary but harmless.

- fixed unnecessary drop table in regression

Ok.

- use the existing table for uniqueness check in 005_opclass_damage.pl

It appears you still create a new table, bttest_unique, rather than using the existing table int4tbl. That's fine.

- added tests into 003_check.pl . It is only smoke test that just verifies new functions.

+
+$node->command_checks_all(
+   [
+       @cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+       '--checkunique', 'db1'
+   ],
+   2,
+   [$index_missing_relation_fork_re],
+   [$no_output_re],
+   'pg_amcheck smoke test --parent-check');
+
+$node->command_checks_all(
+   [
+       @cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+       '--rootdescend', '--checkunique',  'db1'
+   ],
+   2,
+   [$index_missing_relation_fork_re],
+   [$no_output_re],
+   'pg_amcheck smoke test --heapallindexed --rootdescend');
+
+$node->command_checks_all(
+   [ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+   0, [$no_output_re], [$no_output_re],
+   'pg_amcheck excluding all corrupt schemas');
+

You have borrowed the existing tests but forgot to change their names. (The last line of each check is the test name, such as 'pg_amcheck smoke test --parent-check'.) Please make each test name unique.

- added test contrib/amcheck/t/004_verify_nbtree_unique.pl it is more extensive test based on opclass damage which was intended to be main test for amcheck, but which I've forgotten to add to commit in v5.
005_opclass_damage.pl test, which you've seen in v5 is largely based on first part of 004_verify_nbtree_unique.pl (with the later calling pg_amcheck, and the former calling bt_index_check(), bt_index_parent_check() )

Ok.

- added forgotten upgrade script amcheck--1.3--1.4.sql (from v4)

Ok.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

In reply to: Mark Dilger (#22)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

The pstrdup is unnecessary but harmless.

- use the existing table for uniqueness check in 005_opclass_damage.pl

It appears you still create a new table, bttest_unique, rather than using
the existing table int4tbl. That's fine.

- added tests into 003_check.pl . It is only smoke test that just

verifies new functions.

You have borrowed the existing tests but forgot to change their names.
(The last line of each check is the test name, such as 'pg_amcheck smoke
test --parent-check'.) Please make each test name unique.

Thanks for your review! Fixed all these remaining things from patch v6.
PFA v7 patch.

---
Best regards,
Maxim Orlov.

Attachments:

v7-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchapplication/octet-stream; name=v7-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchDownload
From 10c94688e3e0aae2f3ac94026c04161f7b66b04a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 8 Feb 2021 12:26:08 +0400
Subject: [PATCH v7] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  25 ++
 contrib/amcheck/sql/check_btree.sql           |   6 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 327 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  56 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  48 ++-
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  68 +++-
 12 files changed, 791 insertions(+), 31 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..0144767b36e 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,6 +177,31 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..4eb5ffb21d3 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,6 +115,12 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d3b29d3d890..80ddf3c819b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,41 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1707,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1750,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1834,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2005,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index d4a53c8e636..c4711d4a956 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,34 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		if (opts.checkunique == true)
+		{
+			dat->is_checkunique = true;
+
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +906,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +916,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +930,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1139,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1226,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 5913fcc5305..de125ed3c0c 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -8,7 +8,7 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 
 use Fcntl qw(:seek);
-use Test::More tests => 63;
+use Test::More tests => 75;
 
 my ($node, $port, %corrupt_page, %remove_relation);
 
@@ -258,6 +258,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,3 +520,46 @@ $node->command_checks_all(
 	[ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index 2f86f4f2a40..5219c6ec0f5 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -8,7 +8,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 5;
+use Test::More tests => 10;
 
 my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init;
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,3 +76,50 @@ $node->command_checks_all(
 	[],
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
+
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
-- 
2.32.0 (Apple Git-132)

#24Julien Rouhaud
rjuju123@gmail.com
In reply to: Максим Орлов (#23)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi,

On Fri, Dec 24, 2021 at 2:06 AM Максим Орлов <orlovmg@gmail.com> wrote:

Thanks for your review! Fixed all these remaining things from patch v6.
PFA v7 patch.

The cfbot reports that you have mixed declarations and code
(https://cirrus-ci.com/task/6407449413419008):

[17:21:26.926] pg_amcheck.c: In function ‘main’:
[17:21:26.926] pg_amcheck.c:634:4: error: ISO C90 forbids mixed
declarations and code [-Werror=declaration-after-statement]
[17:21:26.926] 634 | int vmaj = 0,
[17:21:26.926] | ^~~

#25Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Julien Rouhaud (#24)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

The cfbot reports that you have mixed declarations and code
(https://cirrus-ci.com/task/6407449413419008):

[17:21:26.926] pg_amcheck.c: In function ‘main’:
[17:21:26.926] pg_amcheck.c:634:4: error: ISO C90 forbids mixed
declarations and code [-Werror=declaration-after-statement]
[17:21:26.926] 634 | int vmaj = 0,
[17:21:26.926] | ^~~

Corrected this, thanks!
Also added more comments on this part of the code.
PFA v8 of a patch

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v8-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchapplication/octet-stream; name=v8-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchDownload
From 5f4a18a7bdd1e5a2e7d63304241fa29b541cfee3 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 12 Jan 2022 11:46:24 +0400
Subject: [PATCH v8] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  25 ++
 contrib/amcheck/sql/check_btree.sql           |   6 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 327 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  60 +++-
 src/bin/pg_amcheck/t/003_check.pl             |  48 ++-
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  68 +++-
 12 files changed, 795 insertions(+), 31 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..0144767b36e 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,6 +177,31 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..4eb5ffb21d3 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,6 +115,12 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d2510ee6480..4e074b56bc5 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,41 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque))
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple).
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1707,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1750,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1834,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2005,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 6607f729382..b3d393c500b 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +910,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +920,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +934,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1143,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1230,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 9df027b37f0..d8d03dabd6e 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -8,7 +8,7 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 
 use Fcntl qw(:seek);
-use Test::More tests => 63;
+use Test::More tests => 75;
 
 my ($node, $port, %corrupt_page, %remove_relation);
 
@@ -258,6 +258,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,3 +520,46 @@ $node->command_checks_all(
 	[ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index d81c9583de2..905d3902c2d 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -8,7 +8,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 5;
+use Test::More tests => 10;
 
 my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init;
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,3 +76,50 @@ $node->command_checks_all(
 	[],
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
+
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
-- 
2.24.3 (Apple Git-128)

#26Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#25)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

By the way I've forgotten to add one part of my code into the CF patch
related to the treatment of NULL values in checking btree unique
constraints.
PFA v9 of a patch with this minor code and tests additions.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v9-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchapplication/octet-stream; name=v9-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-un.patchDownload
From b3793c95f07f5104abfb1542d27d6ba2a67587bf Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 13 Jan 2022 11:56:38 +0400
Subject: [PATCH v9] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  60 +++-
 src/bin/pg_amcheck/t/003_check.pl             |  48 ++-
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  68 +++-
 12 files changed, 822 insertions(+), 31 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..d6d578e9995 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,53 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..8e09f43c373 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,25 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d2510ee6480..4b947558225 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1709,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1752,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1836,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2007,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 6607f729382..b3d393c500b 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +910,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +920,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +934,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1143,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1230,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 9df027b37f0..d8d03dabd6e 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -8,7 +8,7 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 
 use Fcntl qw(:seek);
-use Test::More tests => 63;
+use Test::More tests => 75;
 
 my ($node, $port, %corrupt_page, %remove_relation);
 
@@ -258,6 +258,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,3 +520,46 @@ $node->command_checks_all(
 	[ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index d81c9583de2..905d3902c2d 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -8,7 +8,7 @@ use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 5;
+use Test::More tests => 10;
 
 my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init;
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,3 +76,50 @@ $node->command_checks_all(
 	[],
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
+
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
-- 
2.24.3 (Apple Git-128)

#27Maxim Orlov
orlovmg@gmail.com
In reply to: Pavel Borisov (#26)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

I've updated the patch due to recent changes by Daniel Gustafsson
(549ec201d6132b7).

--
Best regards,
Maxim Orlov.

Attachments:

v10-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/octet-stream; name=v10-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From d23722b8f083ecfec69307e24c4748c9ddb09ff0 Mon Sep 17 00:00:00 2001
From: Maxim Orlov <m.orlov@postgrespro.ru>
Date: Mon, 21 Feb 2022 17:03:59 +0300
Subject: [PATCH v10] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  60 +++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 818 insertions(+), 29 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..d6d578e9995 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,53 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..8e09f43c373 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,25 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d2510ee6480..4b947558225 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1709,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1752,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = (BTPageOpaque) PageGetSpecialPointer(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1836,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2007,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 11d1eb5af23..0f23bbd575b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 6607f729382..b3d393c500b 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +910,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +920,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +934,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1143,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1230,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index d984eacb24f..eb701cb85e3 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -258,6 +258,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -518,4 +521,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index a5e82082700..dcaa333133a 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -58,4 +77,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.25.1

#28Greg Stark
stark@mit.edu
In reply to: Maxim Orlov (#27)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

This patch was broken by d16773cdc86210493a2874cb0cf93f3883fcda73 "Add
macros in hash and btree AMs to get the special area of their pages"

If it's really just a few macros it should be easy enough to merge but
it would be good to do a rebase given the number of other commits
since February anyways.

On Mon, 21 Feb 2022 at 09:14, Maxim Orlov <orlovmg@gmail.com> wrote:

I've updated the patch due to recent changes by Daniel Gustafsson (549ec201d6132b7).

--
Best regards,
Maxim Orlov.

--
greg

#29Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Greg Stark (#28)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

This patch was broken by d16773cdc86210493a2874cb0cf93f3883fcda73 "Add
macros in hash and btree AMs to get the special area of their pages"

If it's really just a few macros it should be easy enough to merge but
it would be good to do a rebase given the number of other commits
since February anyways.

Rebased, thanks!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v11-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/octet-stream; name=v11-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From b2bae648a344bf42b874c41a5d633c949e7609f5 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 4 Apr 2022 12:27:02 +0400
Subject: [PATCH v11] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  60 +++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 818 insertions(+), 29 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 5a3f1ef737c..d6d578e9995 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -177,11 +177,53 @@ SELECT bt_index_check('toasty', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 97a3e1a20d5..8e09f43c373 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -115,11 +115,25 @@ INSERT INTO toast_bug SELECT repeat('a', 2200);
 -- Should not get false positive report of corruption:
 SELECT bt_index_check('toasty', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
 DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 70278c4f932..ddd9c6783e4 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -323,7 +351,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/*
@@ -418,7 +446,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -450,6 +479,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -507,6 +538,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -633,6 +681,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -873,6 +923,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1027,6 +1233,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1049,6 +1258,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1439,6 +1655,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1456,12 +1709,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1495,6 +1752,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1540,9 +1836,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1709,6 +2007,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936f..a8a83e7cc26 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 6607f729382..b3d393c500b 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -449,6 +453,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				fprintf(stderr,
 						_("Try \"%s --help\" for more information.\n"),
@@ -614,6 +621,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -871,7 +910,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -880,11 +920,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -892,6 +934,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1100,17 +1143,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1187,6 +1230,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6b..19a269c1b83 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index a5e82082700..dcaa333133a 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -58,4 +77,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.24.3 (Apple Git-128)

#30Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#29)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

v11 patch do not apply due to recent code changes.
Rebased. PFA v12.

Please feel free to check and discuss it.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v12-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/octet-stream; name=v12-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From 097dbe8593d2b7d4229247c9e65e2136561c9fe8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v12] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  60 +++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 818 insertions(+), 29 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f4..9e257ac3bb2 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d05..5afe7f369d7 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index a8791000f87..470db0698c7 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -344,7 +372,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -445,7 +473,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -477,6 +506,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -534,6 +565,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -660,6 +708,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -900,6 +950,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1054,6 +1260,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1076,6 +1285,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1466,6 +1682,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1483,12 +1736,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1522,6 +1779,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1567,9 +1863,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1736,6 +2034,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936f..a8a83e7cc26 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 48cee8c1c4e..3dfd7e918df 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1074,17 +1117,17 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context)
 
 	if (PQresultStatus(res) == PGRES_TUPLES_OK)
 	{
-		int                     ntups = PQntuples(res);
+		int			ntups = PQntuples(res);
 
 		if (ntups > 1)
 		{
 			/*
 			 * We expect the btree checking functions to return one void row
 			 * each, or zero rows if the check was skipped due to the object
-			 * being in the wrong state to be checked, so we should output some
-			 * sort of warning if we get anything more, not because it
-			 * indicates corruption, but because it suggests a mismatch between
-			 * amcheck and pg_amcheck versions.
+			 * being in the wrong state to be checked, so we should output
+			 * some sort of warning if we get anything more, not because it
+			 * indicates corruption, but because it suggests a mismatch
+			 * between amcheck and pg_amcheck versions.
 			 *
 			 * In conjunction with --progress, anything written to stderr at
 			 * this time would present strangely to the user without an extra
@@ -1161,6 +1204,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6b..19a269c1b83 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index a5e82082700..dcaa333133a 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -58,4 +77,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.24.3 (Apple Git-128)

#31Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#30)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

CFbot says v12 patch does not apply.
Rebased. PFA v13.
Your reviews are very much welcome!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v13-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/x-patch; name=v13-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From 21fe45c0ae8479e0733bd8caeb5d2a19d715e0d9 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v13] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

With 'checkunique' option bt_index_check() and bt_index_parent_check()
for btree indexes that has unique constraint will check it i.e.
will check that only one heap entry for all equal keys in the index
(including posting list entries) is visible. Report error if not.

pg_amcheck called with --checkunique option will do the same for
all indexes it checks

Authors:
Anastasia Lubennikova <lubennikovaav@gmail.com>
Pavel Borisov <pashkin.elfe@gmail.com>
Maxim Orlov <orlovmg@gmail.com>
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  50 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 813 insertions(+), 24 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..1caba148aa4
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f4..9e257ac3bb2 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', false, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d05..5afe7f369d7 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', true, true);
+SELECT bt_index_check('bttest_b_idx', false, true);
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
+
+-- Check null values in unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', true, true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', true, true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..a99e474f1f2
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 looks equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck get uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck get uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index a8791000f87..470db0698c7 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -344,7 +372,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -445,7 +473,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -477,6 +506,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -534,6 +565,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot it to check uniqueness of the index For better
+	 * performance, take it once per index check. If snapshot already taken,
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -660,6 +708,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -900,6 +950,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced from nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print error message for unique constrain violation in the btree
+ * index under WARNING level and set flag to report ERROR at the end of check
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. If TID of any posting list entry is
+	 * visible, and lVis_tid is already valid report duplicate.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique violation between the
+				 * posting list entries of a first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible, save info about
+	 * it for next comparisons in the loop in bt_page_check(). If also
+	 * lVis_tid is already valid, report duplicate.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1054,6 +1260,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint check that only one of table entries for
+ *   equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1076,6 +1285,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1466,6 +1682,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique, verify entries uniqueness by checking heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1483,12 +1736,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1522,6 +1779,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint check that not more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* First key on next page is same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1567,9 +1863,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1736,6 +2034,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936f..a8a83e7cc26 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c04655..61dacf1ee44 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index f0b818e987a..3dfd7e918df 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check version of amcheck extension. Skip requested unique constraint
+		 * check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1161,6 +1204,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6b..19a269c1b83 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index ce376f239cf..81d392a34e6 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.24.3 (Apple Git-128)

#32Aleksander Alekseev
aleksander@timescale.com
In reply to: Pavel Borisov (#31)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi Pavel,

Rebased. PFA v13.
Your reviews are very much welcome!

I noticed that this patch is in "Needs Review" state and it has been
stuck for some time now, so I decided to take a look.

```
+SELECT bt_index_parent_check('bttest_a_idx', true, true, true);
+SELECT bt_index_parent_check('bttest_b_idx', true, false, true);
``

1. This "true, false, true" sequence is difficult to read. I suggest
we use named arguments here.

2. I believe there are some minor issues with the comments. E.g. instead of:

- First key on next page is same
- Make values 768 and 769 looks equal

I would write:

- The first key on the next page is the same
- Make values 768 and 769 look equal

There are many little errors like these.

```
+# Copyright (c) 2021, PostgreSQL Global Development Group
```

3. Oh no. The copyright has expired!

```
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
```

4. This piece of documentation was copy-pasted between two functions
without change of the function name.

Other than that to me the patch looks in pretty good shape. Here is
v14 where I fixed my own nitpicks, with the permission of Pavel given
offlist.

If no one sees any other defects I'm going to change the status of the
patch to "Ready to Committer" in a short time.

--
Best regards,
Aleksander Alekseev

Attachments:

v14-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/octet-stream; name=v14-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From 37553352e796e877329e111b8744844ec5fb64ee Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v14] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

Add 'checkunique' argument to bt_index_check() and bt_index_parent_check().
When the flag is specified the procedures will check the unique constraint
violation for unique indexes. Only one heap entry for all equal keys in
the index should be visible (including posting list entries). Report an error
otherwise.

pg_amcheck called with --checkunique option will do the same check for all
the indexes it checks.

Author: Anastasia Lubennikova <lubennikovaav@gmail.com>
Author: Pavel Borisov <pashkin.elfe@gmail.com>
Author: Maxim Orlov <orlovmg@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Reviewed-by: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 234 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 330 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  50 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 814 insertions(+), 24 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50..88271687a3 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 0000000000..75574eaa64
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f75..e67ace01c9 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f..86b38d93f4 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d0..aa461f7fb9 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 0000000000..1df106a232
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,234 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 6;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 look equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck finds the uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck finds the uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication is possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 2beeebb163..c14bf62ad4 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -344,7 +372,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -445,7 +473,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -477,6 +506,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -534,6 +565,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot to check the uniqueness of the index. For better
+	 * performance take it once per index check. If snapshot already taken
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -660,6 +708,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -900,6 +950,163 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced by nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare and print an error message for unique constrain violation in
+ * a btree index under WARNING level. Also set a flag to report ERROR
+ * at the end of the check.
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. Report duplicate if TID of any posting
+	 * list entry is visible and lVis_tid is valid.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique constraint violation between
+				 * the posting list entries of the first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible save info about
+	 * it for the next comparisons in the loop in bt_page_check(). Report
+	 * duplicate if lVis_tid is already valid.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1054,6 +1261,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint make sure that only one of table entries
+ *   for equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1076,6 +1286,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1466,6 +1683,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique verify entries uniqueness by checking the heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1483,12 +1737,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1522,6 +1780,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint make sure that no more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* The first key on the next page is the same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1567,9 +1864,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1736,6 +2035,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936..b6f3adc612 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_parent_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c0465..61dacf1ee4 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 3cff319f02..3ad5927319 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check the version of amcheck extension. Skip requested unique
+		 * constraint check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1163,6 +1206,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6..19a269c1b8 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index ce376f239c..81d392a34e 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.36.1

#33Dmitry Koval
d.koval@postgrespro.ru
In reply to: Aleksander Alekseev (#32)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi!

I would make two cosmetic changes.

1. I suggest replace description of function bt_report_duplicate() from
```
/*
* Prepare and print an error message for unique constrain violation in
* a btree index under WARNING level. Also set a flag to report ERROR
* at the end of the check.
*/
```
to
```
/*
* Prepare an error message for unique constrain violation in
* a btree index and report ERROR.
*/
```

2. I think will be better to change test 004_verify_nbtree_unique.pl -
replace
```
use Test::More tests => 6;
```
to
```
use Test::More;
...
done_testing();
```
(same as in the other three tests).

--
With best regards,
Dmitry Koval

Postgres Professional: http://postgrespro.com

Attachments:

v15-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchtext/plain; charset=UTF-8; name=v15-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From 93a10abd0afb14b264e4cf59f7e92f619dd9b11a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v15] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

Add 'checkunique' argument to bt_index_check() and bt_index_parent_check().
When the flag is specified the procedures will check the unique constraint
violation for unique indexes. Only one heap entry for all equal keys in
the index should be visible (including posting list entries). Report an error
otherwise.

pg_amcheck called with --checkunique option will do the same check for all
the indexes it checks.

Author: Anastasia Lubennikova <lubennikovaav@gmail.com>
Author: Pavel Borisov <pashkin.elfe@gmail.com>
Author: Maxim Orlov <orlovmg@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Reviewed-by: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 235 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  50 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 814 insertions(+), 24 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50..88271687a3 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 0000000000..75574eaa64
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f75..e67ace01c9 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f..86b38d93f4 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d0..aa461f7fb9 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 0000000000..83572959bd
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,235 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 look equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck finds the uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck finds the uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication is possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
+done_testing();
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 2beeebb163..169a16b82c 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -344,7 +372,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -445,7 +473,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -477,6 +506,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -534,6 +565,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot to check the uniqueness of the index. For better
+	 * performance take it once per index check. If snapshot already taken
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -660,6 +708,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -900,6 +950,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced by nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare an error message for unique constrain violation in
+ * a btree index and report ERROR.
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. Report duplicate if TID of any posting
+	 * list entry is visible and lVis_tid is valid.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique constraint violation between
+				 * the posting list entries of the first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible save info about
+	 * it for the next comparisons in the loop in bt_page_check(). Report
+	 * duplicate if lVis_tid is already valid.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1054,6 +1260,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint make sure that only one of table entries
+ *   for equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1076,6 +1285,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1466,6 +1682,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique verify entries uniqueness by checking the heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1483,12 +1736,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1522,6 +1779,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint make sure that no more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* The first key on the next page is the same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1567,9 +1863,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1736,6 +2034,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936..b6f3adc612 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_parent_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c0465..61dacf1ee4 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index fea35e4b14..956fb6f565 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.no_btree_expansion = false
+	.no_btree_expansion = false,
+	.checkunique = false
 };
 
 static const char *progname = NULL;
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check the version of amcheck extension. Skip requested unique
+		 * constraint check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1163,6 +1206,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6..19a269c1b8 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index ce376f239c..81d392a34e 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.31.0.windows.1

#34Karina Litskevich
litskevichkarina@gmail.com
In reply to: Dmitry Koval (#33)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi,

I also would like to suggest a cosmetic change.
In v15 a new field checkunique is added after heapallindexed and before
no_btree_expansion fields in struct definition, but in initialisation it is
added after no_btree_expansion:

--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
  bool parent_check;
  bool rootdescend;
  bool heapallindexed;
+ bool checkunique;

/* heap and btree hybrid option */
bool no_btree_expansion;
@@ -132,7 +133,8 @@ static AmcheckOptions opts = {
.parent_check = false,
.rootdescend = false,
.heapallindexed = false,
- .no_btree_expansion = false
+ .no_btree_expansion = false,
+ .checkunique = false
};

I suggest to add checkunique field between heapallindexed and
no_btree_expansion fields in initialisation as well as in definition:

@@ -132,6 +133,7 @@ static AmcheckOptions opts = {
.parent_check = false,
.rootdescend = false,
.heapallindexed = false,
+ .checkunique = false,
.no_btree_expansion = false
};

--
Best regards,
Litskevich Karina
Postgres Professional: http://postgrespro.com/

Attachments:

v16-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchtext/x-patch; charset=US-ASCII; name=v16-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From 56fe2b608b46c6c97900bbb63b2169e2997bc8cc Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v16] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

Add 'checkunique' argument to bt_index_check() and bt_index_parent_check().
When the flag is specified the procedures will check the unique constraint
violation for unique indexes. Only one heap entry for all equal keys in
the index should be visible (including posting list entries). Report an error
otherwise.

pg_amcheck called with --checkunique option will do the same check for all
the indexes it checks.

Author: Anastasia Lubennikova <lubennikovaav@gmail.com>
Author: Pavel Borisov <pashkin.elfe@gmail.com>
Author: Maxim Orlov <orlovmg@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Reviewed-by: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 235 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  48 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 12 files changed, 813 insertions(+), 23 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50..88271687a3 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 0000000000..75574eaa64
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f75..e67ace01c9 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f..86b38d93f4 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d0..aa461f7fb9 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 0000000000..83572959bd
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,235 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 look equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck finds the uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck finds the uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication is possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
+done_testing();
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 2beeebb163..169a16b82c 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -79,11 +79,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -138,19 +146,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -190,7 +212,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -203,17 +225,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -227,13 +252,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -243,7 +271,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -344,7 +372,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -445,7 +473,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -477,6 +506,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -534,6 +565,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot to check the uniqueness of the index. For better
+	 * performance take it once per index check. If snapshot already taken
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -660,6 +708,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -900,6 +950,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced by nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare an error message for unique constrain violation in
+ * a btree index and report ERROR.
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. Report duplicate if TID of any posting
+	 * list entry is visible and lVis_tid is valid.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique constraint violation between
+				 * the posting list entries of the first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible save info about
+	 * it for the next comparisons in the loop in bt_page_check(). Report
+	 * duplicate if lVis_tid is already valid.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1054,6 +1260,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint make sure that only one of table entries
+ *   for equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1076,6 +1285,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1466,6 +1682,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique verify entries uniqueness by checking the heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1483,12 +1736,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1522,6 +1779,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint make sure that no more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* The first key on the next page is the same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1567,9 +1863,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1736,6 +2034,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936..b6f3adc612 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_parent_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c0465..61dacf1ee4 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index fea35e4b14..ac9ce2a341 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,6 +133,7 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
+	.checkunique = false,
 	.no_btree_expansion = false
 };
 
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check the version of amcheck extension. Skip requested unique
+		 * constraint check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1163,6 +1206,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6..19a269c1b8 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index ce376f239c..81d392a34e 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.25.1

#35Andres Freund
andres@anarazel.de
In reply to: Pavel Borisov (#31)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi,

On 2022-05-20 17:46:38 +0400, Pavel Borisov wrote:

CFbot says v12 patch does not apply.
Rebased. PFA v13.
Your reviews are very much welcome!

Due to the merge of the meson based build this patch needs to be
adjusted: https://cirrus-ci.com/build/6350479973154816

Looks like you need to add amcheck--1.3--1.4.sql to the list of files to be
installed and t/004_verify_nbtree_unique.pl to the tests.

Greetings,

Andres Freund

#36Maxim Orlov
orlovmg@gmail.com
In reply to: Andres Freund (#35)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Thu, 22 Sept 2022 at 18:13, Andres Freund <andres@anarazel.de> wrote:

Due to the merge of the meson based build this patch needs to be
adjusted: https://cirrus-ci.com/build/6350479973154816

Looks like you need to add amcheck--1.3--1.4.sql to the list of files to be
installed and t/004_verify_nbtree_unique.pl to the tests.

Greetings,

Andres Freund

Thanks! Fixed.

--
Best regards,
Maxim Orlov.

Attachments:

v17-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchapplication/octet-stream; name=v17-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patchDownload
From b7989f959429087328f7b1e521072567154d1167 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 11 May 2022 15:54:13 +0400
Subject: [PATCH v17] Add option for amcheck and pg_amcheck to check unique
 constraint for btree indexes.

Add 'checkunique' argument to bt_index_check() and bt_index_parent_check().
When the flag is specified the procedures will check the unique constraint
violation for unique indexes. Only one heap entry for all equal keys in
the index should be visible (including posting list entries). Report an error
otherwise.

pg_amcheck called with --checkunique option will do the same check for all
the indexes it checks.

Author: Anastasia Lubennikova <lubennikovaav@gmail.com>
Author: Pavel Borisov <pashkin.elfe@gmail.com>
Author: Maxim Orlov <orlovmg@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Reviewed-by: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/meson.build                   |   2 +
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 235 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 329 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  48 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  45 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 13 files changed, 815 insertions(+), 23 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50..88271687a3 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 0000000000..75574eaa64
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f75..e67ace01c9 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f..86b38d93f4 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 1db3d20349..1a550ff762 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -12,6 +12,7 @@ install_data(
   'amcheck--1.0--1.1.sql',
   'amcheck--1.1--1.2.sql',
   'amcheck--1.2--1.3.sql',
+  'amcheck--1.3--1.4.sql',
   kwargs: contrib_data_args,
 )
 
@@ -31,6 +32,7 @@ tests += {
       't/001_verify_heapam.pl',
       't/002_cic.pl',
       't/003_cic_2pc.pl',
+      't/004_verify_nbtree_unique.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d0..aa461f7fb9 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 0000000000..83572959bd
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,235 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 look equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck finds the uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck finds the uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication is possible only for same values, but
+# with different visibility.
+$node->safe_psql('postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql('postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql('postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok($stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
+done_testing();
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 9021d156eb..cc75697798 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -80,11 +80,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -139,19 +147,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -191,7 +213,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -204,17 +226,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -228,13 +253,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -244,7 +272,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -345,7 +373,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -446,7 +474,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -478,6 +507,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -535,6 +566,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot to check the uniqueness of the index. For better
+	 * performance take it once per index check. If snapshot already taken
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -661,6 +709,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -901,6 +951,162 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced by nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare an error message for unique constrain violation in
+ * a btree index and report ERROR.
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\": "
+					"Index %s%s and%s%s "
+					"(point to heap %s and %s) page lsn=%X/%X.",
+					RelationGetRelationName(state->rel),
+					itid, pposting, nitid, pnposting, htid, nhtid,
+					LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i, ItemPointer *lVis_tid,
+					  OffsetNumber *lVis_offset, BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. Report duplicate if TID of any posting
+	 * list entry is visible and lVis_tid is valid.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique constraint violation between
+				 * the posting list entries of the first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible save info about
+	 * it for the next comparisons in the loop in bt_page_check(). Report
+	 * duplicate if lVis_tid is already valid.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) "
+						"in index \"%s\". It doesn't have visible heap tids and key "
+						"is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)). "
+						"Vacuum the table and repeat the check.",
+						targetblock, offset,
+						RelationGetRelationName(state->rel),
+						*lVis_block, *lVis_offset, posting,
+						ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						ItemPointerGetOffsetNumberNoCheck(*lVis_tid))));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1055,6 +1261,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint make sure that only one of table entries
+ *   for equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1077,6 +1286,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1467,6 +1683,43 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique verify entries uniqueness by checking the heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset, &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique && P_ISLEAF(topaque) &&
+			OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1484,12 +1737,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1523,6 +1780,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint make sure that no more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later
+				 */
+				rightkey->scantid = NULL;
+
+				/* The first key on the next page is the same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1568,9 +1864,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1737,6 +2035,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 5d61a33936..b6f3adc612 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_parent_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index cfef6c0465..61dacf1ee4 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 9ce4b11f1e..6c27f857d1 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,6 +133,7 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
+	.checkunique = false,
 	.no_btree_expansion = false
 };
 
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check the version of amcheck extension. Skip requested unique
+		 * constraint check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1163,6 +1206,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 0cf67065d6..19a269c1b8 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,46 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique',  'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ],
+	0, [$no_output_re], [$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', 'db3' ],
+		0,
+		[$no_output_re],
+		[qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index ce376f239c..81d392a34e 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.37.0 (Apple Git-136)

#37Maxim Orlov
orlovmg@gmail.com
In reply to: Maxim Orlov (#36)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi!

I think, this patch was marked as "Waiting on Author", probably, by
mistake. Since recent changes were done without any significant code
changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

--
Best regards,
Maxim Orlov.

#38Aleksander Alekseev
aleksander@timescale.com
In reply to: Maxim Orlov (#37)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi hackers,

I think, this patch was marked as "Waiting on Author", probably, by mistake. Since recent changes were done without any significant code changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

--
Best regards,
Aleksander Alekseev

#39Alexander Korotkov
aekorotkov@gmail.com
In reply to: Aleksander Alekseev (#38)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

I think, this patch was marked as "Waiting on Author", probably, by mistake. Since recent changes were done without any significant code changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

This patch seems useful to me. I went through the thread, it seems
that all the critics are addressed.

I've rebased this patch. Also, I've run perltidy for tests, split
long errmsg() into errmsg(), errdetail() and errhint(), and do other
minor enchantments.

I think this patch is ready to go. I'm going to push it if there are
no objections.

------
Regards,
Alexander Korotkov

Attachments:

0001-Teach-contrib-amcheck-to-check-the-unique-constr-v18.patchapplication/octet-stream; name=0001-Teach-contrib-amcheck-to-check-the-unique-constr-v18.patchDownload
From a405bdcbca38a9f44da70a20312a9bc81e50c76c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Tue, 24 Oct 2023 22:07:30 +0300
Subject: [PATCH] Teach contrib/amcheck to check the unique constraint
 violation

Add the 'checkunique' argument to bt_index_check() and bt_index_parent_check().
When the flag is specified the procedures will check the unique constraint
violation for unique indexes.  Only one heap entry for all equal keys in
the index should be visible (including posting list entries).  Report an error
otherwise.

pg_amcheck called with the --checkunique option will do the same check for all
the indexes it checks.

Author: Anastasia Lubennikova <lubennikovaav@gmail.com>
Author: Pavel Borisov <pashkin.elfe@gmail.com>
Author: Maxim Orlov <orlovmg@gmail.com>
Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Reviewed-by: Aleksander Alekseev <aleksander@timescale.com>
Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
---
 contrib/amcheck/Makefile                      |   2 +-
 contrib/amcheck/amcheck--1.3--1.4.sql         |  29 ++
 contrib/amcheck/amcheck.control               |   2 +-
 contrib/amcheck/expected/check_btree.out      |  42 +++
 contrib/amcheck/meson.build                   |   2 +
 contrib/amcheck/sql/check_btree.sql           |  14 +
 contrib/amcheck/t/004_verify_nbtree_unique.pl | 244 +++++++++++++
 contrib/amcheck/verify_nbtree.c               | 330 +++++++++++++++++-
 doc/src/sgml/amcheck.sgml                     |  14 +-
 doc/src/sgml/ref/pg_amcheck.sgml              |  11 +
 src/bin/pg_amcheck/pg_amcheck.c               |  48 ++-
 src/bin/pg_amcheck/t/003_check.pl             |  50 +++
 src/bin/pg_amcheck/t/005_opclass_damage.pl    |  65 ++++
 13 files changed, 830 insertions(+), 23 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.3--1.4.sql
 create mode 100644 contrib/amcheck/t/004_verify_nbtree_unique.pl

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b82f221e50b..88271687a3e 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -7,7 +7,7 @@ OBJS = \
 	verify_nbtree.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
+DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
 REGRESS = check check_btree check_heap
diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
new file mode 100644
index 00000000000..75574eaa64b
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -0,0 +1,29 @@
+/* contrib/amcheck/amcheck--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.4,
+-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
+-- and 1.1 bt_index_check signature.
+
+--
+-- bt_index_parent_check()
+--
+CREATE FUNCTION bt_index_parent_check(index regclass,
+    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_parent_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+--
+-- bt_index_check()
+--
+CREATE FUNCTION bt_index_check(index regclass,
+    heapallindexed boolean, checkunique boolean)
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'bt_index_check'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+-- We don't want this to be available to public
+REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
+REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control
index ab50931f754..e67ace01c99 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.3'
+default_version = '1.4'
 module_pathname = '$libdir/amcheck'
 relocatable = true
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index 38791bbc1f4..86b38d93f41 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -199,6 +199,47 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
  
 (1 row)
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+ bt_index_parent_check 
+-----------------------
+ 
+(1 row)
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+ bt_index_check 
+----------------
+ 
+(1 row)
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -206,5 +247,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build
index 5b55cf343a9..4c8e2e2f13c 100644
--- a/contrib/amcheck/meson.build
+++ b/contrib/amcheck/meson.build
@@ -23,6 +23,7 @@ install_data(
   'amcheck--1.0--1.1.sql',
   'amcheck--1.1--1.2.sql',
   'amcheck--1.2--1.3.sql',
+  'amcheck--1.3--1.4.sql',
   kwargs: contrib_data_args,
 )
 
@@ -42,6 +43,7 @@ tests += {
       't/001_verify_heapam.pl',
       't/002_cic.pl',
       't/003_cic_2pc.pl',
+      't/004_verify_nbtree_unique.pl',
     ],
   },
 }
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 033c04b4d05..aa461f7fb97 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -135,6 +135,19 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
+-- UNIQUE constraint check
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+
+-- Check that null values in an unique index are not treated as equal
+CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
+INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+CREATE INDEX on bttest_unique_nulls (b,c);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+
 -- cleanup
 DROP TABLE bttest_a;
 DROP TABLE bttest_b;
@@ -142,5 +155,6 @@ DROP TABLE bttest_multi;
 DROP TABLE delete_test_table;
 DROP TABLE toast_bug;
 DROP FUNCTION ifun(int8);
+DROP TABLE bttest_unique_nulls;
 DROP OWNED BY regress_bttest_role; -- permissions
 DROP ROLE regress_bttest_role;
diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl
new file mode 100644
index 00000000000..b999ab9c176
--- /dev/null
+++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl
@@ -0,0 +1,244 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create a custom operator class and an index which uses it.
+$node->safe_psql(
+	'postgres', q(
+	CREATE EXTENSION amcheck;
+
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	---
+	--- Check 1: uniqueness violation.
+	---
+	CREATE FUNCTION ok_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	---
+	--- Make values 768 and 769 look equal.
+	---
+	CREATE FUNCTION bad_cmp1 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 ELSE ok_cmp($1, $2)
+			END;
+	$$;
+
+	---
+	--- Check 2: uniqueness violation without deduplication.
+	---
+	CREATE FUNCTION ok_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp2 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
+			ELSE ok_cmp($1, $2)
+		END;
+	$$;
+
+	---
+	--- Check 3: uniqueness violation with deduplication.
+	---
+	CREATE FUNCTION ok_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT ok_cmp($1, $2);
+	$$;
+
+	CREATE FUNCTION bad_cmp3 (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT bad_cmp2($1, $2);
+	$$;
+
+	---
+	--- Create data.
+	---
+	CREATE TABLE bttest_unique1 (i int4);
+	INSERT INTO bttest_unique1
+		(SELECT * FROM generate_series(1, 1024) gs);
+
+	CREATE TABLE bttest_unique2 (i int4);
+	INSERT INTO bttest_unique2(i)
+		(SELECT * FROM generate_series(1, 400) gs);
+	INSERT INTO bttest_unique2
+		(SELECT * FROM generate_series(400, 1024) gs);
+
+	CREATE TABLE bttest_unique3 (i int4);
+	INSERT INTO bttest_unique3
+		SELECT * FROM bttest_unique2;
+
+	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
+	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
+
+	CREATE UNIQUE INDEX bttest_unique_idx1
+						ON bttest_unique1
+						USING btree (i int4_custom_ops1)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx2
+						ON bttest_unique2
+						USING btree (i int4_custom_ops2)
+						WITH (deduplicate_items = off);
+	CREATE UNIQUE INDEX bttest_unique_idx3
+						ON bttest_unique3
+						USING btree (i int4_custom_ops3)
+						WITH (deduplicate_items = on);
+));
+
+my ($result, $stdout, $stderr);
+
+#
+# Test 1.
+#  - insert seq values
+#  - create unique index
+#  - break cmp function
+#  - amcheck finds the uniqueness violation
+#
+
+# We have not yet broken the index, so we should get no corruption
+$result = $node->safe_psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
+
+# Change the operator class to use a function which considers certain different
+# values to be equal.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'bad_cmp1'::regproc
+	WHERE amproc = 'ok_cmp1'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx1', true, true);
+));
+ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
+	'detected uniqueness violation for index "bttest_unique_idx1"');
+
+#
+# Test 2.
+#  - break cmp function
+#  - insert seq values with duplicates
+#  - create unique index
+#  - make cmp function correct
+#  - amcheck finds the uniqueness violation
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
+	'detected item order invariant violation for index "bttest_unique_idx2"');
+
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp2'::regproc
+	WHERE amproc = 'bad_cmp2'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx2', true, true);
+));
+ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
+	'detected uniqueness violation for index "bttest_unique_idx2"');
+
+#
+# Test 3.
+#  - same as Test 2, but with index deduplication
+#
+# Then uniqueness violation is detected between different posting list
+# entries inside one index entry.
+#
+
+# Due to bad cmp function we expect amcheck to detect item order violation,
+# but no uniqueness violation.
+($result, $stdout, $stderr) = $node->psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
+	'detected item order invariant violation for index "bttest_unique_idx3"');
+
+# For unique index deduplication is possible only for same values, but
+# with different visibility.
+$node->safe_psql(
+	'postgres', q(
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
+	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
+	INSERT INTO bttest_unique3 VALUES (400);
+));
+
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc SET
+		   amproc = 'ok_cmp3'::regproc
+	WHERE amproc = 'bad_cmp3'::regproc;
+));
+
+($result, $stdout, $stderr) = $node->psql(
+	'postgres', q(
+	SELECT bt_index_check('bttest_unique_idx3', true, true);
+));
+ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
+	'detected uniqueness violation for index "bttest_unique_idx3"');
+
+$node->stop;
+done_testing();
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3e07a3e35f4..7282cf7fc80 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -81,11 +81,19 @@ typedef struct BtreeCheckState
 	bool		heapallindexed;
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
+	/* Also check uniqueness constraint if index is unique */
+	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
 	BufferAccessStrategy checkstrategy;
 
+	/*
+	 * Info for uniqueness checking. Fill these fields once per index check.
+	 */
+	IndexInfo  *indexinfo;
+	Snapshot	snapshot;
+
 	/*
 	 * Mutable state, for verification of particular page:
 	 */
@@ -140,19 +148,33 @@ PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
-									bool heapallindexed, bool rootdescend);
+									bool heapallindexed, bool rootdescend,
+									bool checkunique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend);
+								 bool rootdescend, bool checkunique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
+static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
+static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
+								BlockNumber block, OffsetNumber offset,
+								int posting, ItemPointer nexttid,
+								BlockNumber nblock, OffsetNumber noffset,
+								int nposting);
+static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+								  BlockNumber targetblock,
+								  OffsetNumber offset, int *lVis_i,
+								  ItemPointer *lVis_tid,
+								  OffsetNumber *lVis_offset,
+								  BlockNumber *lVis_block);
 static void bt_target_page_check(BtreeCheckState *state);
-static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state);
+static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
+												OffsetNumber *rightfirstoffset);
 static void bt_child_check(BtreeCheckState *state, BTScanInsert targetkey,
 						   OffsetNumber downlinkoffnum);
 static void bt_child_highkey_check(BtreeCheckState *state,
@@ -192,7 +214,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -205,17 +227,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
+	bool		checkunique = false;
 
-	if (PG_NARGS() == 2)
+	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
+	if (PG_NARGS() == 3)
+		checkunique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -229,13 +254,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
+	bool		checkunique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
-	if (PG_NARGS() == 3)
+	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
+	if (PG_NARGS() == 4)
+		checkunique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
 
 	PG_RETURN_VOID();
 }
@@ -245,7 +273,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend)
+						bool rootdescend, bool checkunique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -356,7 +384,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend);
+							 heapallindexed, rootdescend, checkunique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -457,7 +485,8 @@ btree_index_mainfork_expected(Relation rel)
  */
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
-					 bool readonly, bool heapallindexed, bool rootdescend)
+					 bool readonly, bool heapallindexed, bool rootdescend,
+					 bool checkunique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -489,6 +518,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
+	state->checkunique = checkunique;
+	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
 	{
@@ -546,6 +577,23 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		}
 	}
 
+	/*
+	 * We need a snapshot to check the uniqueness of the index. For better
+	 * performance take it once per index check. If snapshot already taken
+	 * reuse it.
+	 */
+	if (state->checkunique)
+	{
+		state->indexinfo = BuildIndexInfo(state->rel);
+		if (state->indexinfo->ii_Unique)
+		{
+			if (snapshot != SnapshotAny)
+				state->snapshot = snapshot;
+			else
+				state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
+		}
+	}
+
 	Assert(!state->rootdescend || state->readonly);
 	if (state->rootdescend && !state->heapkeyspace)
 		ereport(ERROR,
@@ -672,6 +720,8 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	}
 
 	/* Be tidy: */
+	if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot)
+		UnregisterSnapshot(state->snapshot);
 	MemoryContextDelete(state->targetcontext);
 }
 
@@ -912,6 +962,161 @@ nextpage:
 	return nextleveldown;
 }
 
+/* Check visibility of the table entry referenced by nbtree index */
+static bool
+heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
+{
+	bool		tid_visible;
+
+	TupleTableSlot *slot = table_slot_create(state->heaprel, NULL);
+
+	tid_visible = table_tuple_fetch_row_version(state->heaprel,
+												tid, state->snapshot, slot);
+	if (slot != NULL)
+		ExecDropSingleTupleTableSlot(slot);
+
+	return tid_visible;
+}
+
+/*
+ * Prepare an error message for unique constrain violation in
+ * a btree index and report ERROR.
+ */
+static void
+bt_report_duplicate(BtreeCheckState *state,
+					ItemPointer tid, BlockNumber block, OffsetNumber offset,
+					int posting,
+					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
+					int nposting)
+{
+	char	   *htid,
+			   *nhtid,
+			   *itid,
+			   *nitid = "",
+			   *pposting = "",
+			   *pnposting = "";
+
+	htid = psprintf("tid=(%u,%u)",
+					ItemPointerGetBlockNumberNoCheck(tid),
+					ItemPointerGetOffsetNumberNoCheck(tid));
+	nhtid = psprintf("tid=(%u,%u)",
+					 ItemPointerGetBlockNumberNoCheck(nexttid),
+					 ItemPointerGetOffsetNumberNoCheck(nexttid));
+	itid = psprintf("tid=(%u,%u)", block, offset);
+
+	if (nblock != block || noffset != offset)
+		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
+
+	if (posting >= 0)
+		pposting = psprintf(" posting %u", posting);
+
+	if (nposting >= 0)
+		pnposting = psprintf(" posting %u", nposting);
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INDEX_CORRUPTED),
+			 errmsg("index uniqueness is violated for index \"%s\"",
+					RelationGetRelationName(state->rel)),
+			 errdetail("Index %s%s and%s%s (point to heap %s and %s) page lsn=%X/%X.",
+					   itid, pposting, nitid, pnposting, htid, nhtid,
+					   LSN_FORMAT_ARGS(state->targetlsn))));
+}
+
+/* Check if current nbtree leaf entry complies with UNIQUE constraint */
+static void
+bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
+					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i,
+					  ItemPointer *lVis_tid, OffsetNumber *lVis_offset,
+					  BlockNumber *lVis_block)
+{
+	ItemPointer tid;
+	bool		has_visible_entry = false;
+
+	Assert(targetblock != P_NONE);
+
+	/*
+	 * Current tuple has posting list. Report duplicate if TID of any posting
+	 * list entry is visible and lVis_tid is valid.
+	 */
+	if (BTreeTupleIsPosting(itup))
+	{
+		for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+		{
+			tid = BTreeTupleGetPostingN(itup, i);
+			if (heap_entry_is_visible(state, tid))
+			{
+				has_visible_entry = true;
+				if (ItemPointerIsValid(*lVis_tid))
+				{
+					bt_report_duplicate(state,
+										*lVis_tid, *lVis_block,
+										*lVis_offset, *lVis_i,
+										tid, targetblock,
+										offset, i);
+				}
+
+				/*
+				 * Prevent double reporting unique constraint violation between
+				 * the posting list entries of the first tuple on the page after
+				 * cross-page check.
+				 */
+				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+					return;
+
+				*lVis_i = i;
+				*lVis_tid = tid;
+				*lVis_offset = offset;
+				*lVis_block = targetblock;
+			}
+		}
+	}
+
+	/*
+	 * Current tuple has no posting list. If TID is visible save info about
+	 * it for the next comparisons in the loop in bt_page_check(). Report
+	 * duplicate if lVis_tid is already valid.
+	 */
+	else
+	{
+		tid = BTreeTupleGetHeapTID(itup);
+		if (heap_entry_is_visible(state, tid))
+		{
+			has_visible_entry = true;
+			if (ItemPointerIsValid(*lVis_tid))
+			{
+				bt_report_duplicate(state,
+									*lVis_tid, *lVis_block,
+									*lVis_offset, *lVis_i,
+									tid, targetblock,
+									offset, -1);
+			}
+			*lVis_i = -1;
+			*lVis_tid = tid;
+			*lVis_offset = offset;
+			*lVis_block = targetblock;
+		}
+	}
+
+	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
+		*lVis_block != targetblock)
+	{
+		char	   *posting = "";
+
+		if (*lVis_i >= 0)
+			posting = psprintf(" posting %u", *lVis_i);
+		ereport(DEBUG1,
+				(errcode(ERRCODE_NO_DATA),
+				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) in index \"%s\"",
+						targetblock, offset,
+						RelationGetRelationName(state->rel)),
+				 errdetail("It doesn't have visible heap tids and key is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)).",
+						   *lVis_block, *lVis_offset, posting,
+						   ItemPointerGetBlockNumberNoCheck(*lVis_tid),
+						   ItemPointerGetOffsetNumberNoCheck(*lVis_tid)),
+				 errhint("VACUUM the table and repeat the check.")));
+	}
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -1066,6 +1271,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
  * - Various checks on the structure of tuples themselves.  For example, check
  *	 that non-pivot tuples have no truncated attributes.
  *
+ * - For index with unique constraint make sure that only one of table entries
+ *   for equal keys is visible.
+ *
  * Furthermore, when state passed shows ShareLock held, function also checks:
  *
  * - That all child pages respect strict lower bound from parent's pivot
@@ -1088,6 +1296,13 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
+	/* last visible entry info for checking indexes with unique constraint */
+	int			lVis_i = -1;	/* the position of last visible item for
+								 * posting tuple. for non-posting tuple (-1) */
+	ItemPointer lVis_tid = NULL;
+	BlockNumber lVis_block = InvalidBlockNumber;
+	OffsetNumber lVis_offset = InvalidOffsetNumber;
+
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
 
@@ -1478,6 +1693,45 @@ bt_target_page_check(BtreeCheckState *state)
 										LSN_FORMAT_ARGS(state->targetlsn))));
 		}
 
+		/*
+		 * If the index is unique verify entries uniqueness by checking the heap
+		 * tuples visibility.
+		 */
+		if (state->checkunique && state->indexinfo->ii_Unique &&
+			P_ISLEAF(topaque) && !skey->anynullkeys)
+			bt_entry_unique_check(state, itup, state->targetblock, offset,
+								  &lVis_i, &lVis_tid, &lVis_offset,
+								  &lVis_block);
+
+		if (state->checkunique && state->indexinfo->ii_Unique &&
+			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
+		{
+			/* Save current scankey tid */
+			scantid = skey->scantid;
+
+			/*
+			 * Invalidate scankey tid to make _bt_compare compare only keys in
+			 * the item to report equality even if heap TIDs are different
+			 */
+			skey->scantid = NULL;
+
+			/*
+			 * If next key tuple is different, invalidate last visible entry
+			 * data (whole index tuple or last posting in index tuple). Key
+			 * containing null value does not violate unique constraint and
+			 * treated as different to any other key.
+			 */
+			if (_bt_compare(state->rel, skey, state->target,
+							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
+			{
+				lVis_i = -1;
+				lVis_tid = NULL;
+				lVis_block = InvalidBlockNumber;
+				lVis_offset = InvalidOffsetNumber;
+			}
+			skey->scantid = scantid;	/* Restore saved scan key state */
+		}
+
 		/*
 		 * * Last item check *
 		 *
@@ -1495,12 +1749,16 @@ bt_target_page_check(BtreeCheckState *state)
 		 * available from sibling for various reasons, though (e.g., target is
 		 * the rightmost page on level).
 		 */
-		else if (offset == max)
+		if (offset == max)
 		{
 			BTScanInsert rightkey;
+			BlockNumber rightblock_number;
+
+			/* first offset on a right index page (log only) */
+			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
 
 			/* Get item in next/right page */
-			rightkey = bt_right_page_check_scankey(state);
+			rightkey = bt_right_page_check_scankey(state, &rightfirstoffset);
 
 			if (rightkey &&
 				!invariant_g_offset(state, rightkey, max))
@@ -1534,6 +1792,45 @@ bt_target_page_check(BtreeCheckState *state)
 											state->targetblock, offset,
 											LSN_FORMAT_ARGS(state->targetlsn))));
 			}
+
+			/*
+			 * If index has unique constraint make sure that no more than one
+			 * found equal items is visible.
+			 */
+			rightblock_number = topaque->btpo_next;
+			if (state->checkunique && state->indexinfo->ii_Unique &&
+				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+			{
+				elog(DEBUG2, "check cross page unique condition");
+
+				/*
+				 * Make _bt_compare compare only index keys without heap TIDs.
+				 * rightkey->scantid is modified destructively but it is ok
+				 * for it is not used later.
+				 */
+				rightkey->scantid = NULL;
+
+				/* The first key on the next page is the same */
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				{
+					elog(DEBUG2, "cross page equal keys");
+					state->target = palloc_btree_page(state,
+													  rightblock_number);
+					topaque = BTPageGetOpaque(state->target);
+
+					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
+						break;
+
+					itemid = PageGetItemIdCareful(state, rightblock_number,
+												  state->target,
+												  rightfirstoffset);
+					itup = (IndexTuple) PageGetItem(state->target, itemid);
+
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
+										  &lVis_i, &lVis_tid, &lVis_offset,
+										  &lVis_block);
+				}
+			}
 		}
 
 		/*
@@ -1579,9 +1876,11 @@ bt_target_page_check(BtreeCheckState *state)
  *
  * Note that !readonly callers must reverify that target page has not
  * been concurrently deleted.
+ *
+ * Save rightfirstdataoffset for detailed error message.
  */
 static BTScanInsert
-bt_right_page_check_scankey(BtreeCheckState *state)
+bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffset)
 {
 	BTPageOpaque opaque;
 	ItemId		rightitem;
@@ -1748,6 +2047,7 @@ bt_right_page_check_scankey(BtreeCheckState *state)
 		/* Return first data item (if any) */
 		rightitem = PageGetItemIdCareful(state, targetnext, rightpage,
 										 P_FIRSTDATAKEY(opaque));
+		*rightfirstoffset = P_FIRSTDATAKEY(opaque);
 	}
 	else if (!P_ISLEAF(opaque) &&
 			 nline >= OffsetNumberNext(P_FIRSTDATAKEY(opaque)))
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 2b9c1a9205f..780fd05a73b 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -58,7 +58,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -115,7 +115,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When a routine, lightweight test for
+      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When a routine, lightweight test for
       corruption is required in a live production environment, using
       <function>bt_index_check</function> often provides the best
       trade-off between thoroughness of verification and limiting the
@@ -126,7 +129,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -139,7 +142,10 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When the optional <parameter>rootdescend</parameter>
+      index.  When <parameter>checkunique</parameter>
+      is <literal>true</literal> <function>bt_index_parent_check</function> will
+      check that no more than one among duplicate entries in unique
+      index is visible.  When the optional <parameter>rootdescend</parameter>
       argument is <literal>true</literal>, verification re-finds
       tuples on the leaf level by performing a new search from the
       root page for each tuple.  The checks that can be performed by
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index 20c2897accb..067c806b46d 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -432,6 +432,17 @@ PostgreSQL documentation
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><option>--checkunique</option></term>
+     <listitem>
+      <para>
+       For each index with unique constraint checked, verify that no more than
+       one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
+       <option>checkunique</option> option.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 8ac7051ff4d..57c7c1917c4 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,6 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
+	bool		checkunique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -132,6 +133,7 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
+	.checkunique = false,
 	.no_btree_expansion = false
 };
 
@@ -148,6 +150,7 @@ typedef struct DatabaseInfo
 {
 	char	   *datname;
 	char	   *amcheck_schema; /* escaped, quoted literal */
+	bool		is_checkunique;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -267,6 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
+		{"checkunique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -434,6 +438,9 @@ main(int argc, char *argv[])
 				if (optarg)
 					opts.install_schema = pg_strdup(optarg);
 				break;
+			case 14:
+				opts.checkunique = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -589,6 +596,38 @@ main(int argc, char *argv[])
 						PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema);
 		dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema,
 												 strlen(amcheck_schema));
+
+		/*
+		 * Check the version of amcheck extension. Skip requested unique
+		 * constraint check with warning if it is not yet supported by amcheck.
+		 */
+		if (opts.checkunique == true)
+		{
+			/*
+			 * Now amcheck has only major and minor versions in the string but
+			 * we also support revision just in case. Now it is expected to be
+			 * zero.
+			 */
+			int			vmaj = 0,
+						vmin = 0,
+						vrev = 0;
+			const char *amcheck_version = PQgetvalue(result, 0, 1);
+
+			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
+
+			/*
+			 * checkunique option is supported in amcheck since version 1.4
+			 */
+			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
+			{
+				pg_log_warning("--checkunique option is not supported by amcheck "
+							   "version \"%s\"", amcheck_version);
+				dat->is_checkunique = false;
+			}
+			else
+				dat->is_checkunique = true;
+		}
+
 		PQclear(result);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -845,7 +884,8 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 	if (opts.parent_check)
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_parent_check("
-						  "index := c.oid, heapallindexed := %s, rootdescend := %s)"
+						  "index := c.oid, heapallindexed := %s, rootdescend := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -854,11 +894,13 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
 						  "SELECT %s.bt_index_check("
-						  "index := c.oid, heapallindexed := %s)"
+						  "index := c.oid, heapallindexed := %s "
+						  "%s)"
 						  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i "
 						  "WHERE c.oid = %u "
 						  "AND c.oid = i.indexrelid "
@@ -866,6 +908,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
+						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1163,6 +1206,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
+	printf(_("      --checkunique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index d577cffa30d..2b7ef198552 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -257,6 +257,9 @@ for my $dbname (qw(db1 db2 db3))
 
 			CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir);
 			CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir);
+
+			CREATE UNIQUE INDEX t1_btree_unique ON $schema.t1 USING BTREE (i);
+			CREATE UNIQUE INDEX t2_btree_unique ON $schema.t2 USING BTREE (i);
 		));
 	}
 }
@@ -517,4 +520,51 @@ $node->command_checks_all(
 	0, [$no_output_re], [$no_output_re],
 	'pg_amcheck excluding all corrupt schemas');
 
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
+		'--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --parent-check --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
+		'--rootdescend', '--checkunique', 'db1'
+	],
+	2,
+	[$index_missing_relation_fork_re],
+	[$no_output_re],
+	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+
+$node->command_checks_all(
+	[
+		@cmd, '--checkunique', '-d', 'db1', '-d', 'db2',
+		'-d', 'db3', '-S', 's*'
+	],
+	0,
+	[$no_output_re],
+	[$no_output_re],
+	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+
+#
+# Smoke test for checkunique option for not supported versions.
+#
+$node->safe_psql(
+	'db3', q(
+		DROP EXTENSION amcheck;
+		CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ;
+));
+
+$node->command_checks_all(
+	[ @cmd, '--checkunique', 'db3' ],
+	0,
+	[$no_output_re],
+	[
+		qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+	],
+	'pg_amcheck smoke test --checkunique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index fd476179f49..a5ef2c0f33d 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -22,14 +22,33 @@ $node->safe_psql(
 	CREATE FUNCTION int4_asc_cmp (a int4, b int4) RETURNS int LANGUAGE sql AS $$
 		SELECT CASE WHEN $1 = $2 THEN 0 WHEN $1 > $2 THEN 1 ELSE -1 END; $$;
 
+	CREATE FUNCTION ok_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
 	CREATE OPERATOR CLASS int4_fickle_ops FOR TYPE int4 USING btree AS
 	    OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
 	    OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
 	    OPERATOR 5 > (int4, int4), FUNCTION 1 int4_asc_cmp(int4, int4);
 
+	CREATE OPERATOR CLASS int4_unique_ops FOR TYPE int4 USING btree AS
+		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
+		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
+		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp(int4, int4);
+
 	CREATE TABLE int4tbl (i int4);
 	INSERT INTO int4tbl (SELECT * FROM generate_series(1,1000) gs);
 	CREATE INDEX fickleidx ON int4tbl USING btree (i int4_fickle_ops);
+	CREATE UNIQUE INDEX bttest_unique_idx
+						ON int4tbl
+						USING btree (i int4_unique_ops)
+						WITH (deduplicate_items = off);
 ));
 
 # We have not yet broken the index, so we should get no corruption
@@ -57,4 +76,50 @@ $node->command_checks_all(
 	'pg_amcheck all schemas, tables and indexes reports fickleidx corruption'
 );
 
+#
+# Check unique constraints
+#
+
+# Repair broken opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'int4_asc_cmp'::regproc
+		WHERE amproc = 'int4_desc_cmp'::regproc
+));
+
+# We should get no corruptions
+$node->command_like(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	qr/^$/,
+	'pg_amcheck all schemas, tables and indexes reports no corruption');
+
+# Break opclass for check unique tests.
+$node->safe_psql(
+	'postgres', q(
+	CREATE FUNCTION bad_cmp (int4, int4)
+	RETURNS int LANGUAGE sql AS
+	$$
+		SELECT
+			CASE WHEN ($1 = 768 AND $2 = 769) OR
+					  ($1 = 769 AND $2 = 768) THEN 0
+				 WHEN $1 < $2 THEN -1
+				 WHEN $1 > $2 THEN  1
+				 ELSE 0
+			END;
+	$$;
+
+	UPDATE pg_catalog.pg_amproc
+		SET amproc = 'bad_cmp'::regproc
+		WHERE amproc = 'ok_cmp'::regproc
+));
+
+# Unique index corruption should now be reported
+$node->command_checks_all(
+	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	2,
+	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
+	[],
+	'pg_amcheck all schemas, tables and indexes reports bttest_unique_idx corruption'
+);
 done_testing();
-- 
2.39.3 (Apple Git-145)

#40Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Alexander Korotkov (#39)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Alexander!

On Wed, 25 Oct 2023 at 00:13, Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

I think, this patch was marked as "Waiting on Author", probably, by mistake. Since recent changes were done without any significant code changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

This patch seems useful to me. I went through the thread, it seems
that all the critics are addressed.

I've rebased this patch. Also, I've run perltidy for tests, split
long errmsg() into errmsg(), errdetail() and errhint(), and do other
minor enchantments.

I think this patch is ready to go. I'm going to push it if there are
no objections.

------
Regards,
Alexander Korotkov

It's very good that this long-standing patch is finally committed. Thanks a lot!

Regards,
Pavel Borisov

#41Noah Misch
noah@leadboat.com
In reply to: Pavel Borisov (#40)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, Oct 30, 2023 at 11:29:04AM +0400, Pavel Borisov wrote:

On Wed, 25 Oct 2023 at 00:13, Alexander Korotkov <aekorotkov@gmail.com> wrote:

I think this patch is ready to go. I'm going to push it if there are
no objections.

It's very good that this long-standing patch is finally committed. Thanks a lot!

Agreed. I gave this feature (commit 5ae2087) a try. Thanks for implementing
it. Could I get your input on two topics?

==== 1. Cross-page comparison at "first key on the next page" only

Cross-page comparisons got this discussion upthread:

On Tue, Mar 02, 2021 at 07:10:32PM -0800, Peter Geoghegan wrote:

On Mon, Feb 8, 2021 at 2:46 AM Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Caveat: if the first entry on the next index page has a key equal to the key on a previous page AND all heap tid's corresponding to this entry are invisible, currently cross-page check can not detect unique constraint violation between previous index page entry and 2nd, 3d and next current index page entries. In this case, there would be a message that recommends doing VACUUM to remove the invisible entries from the index and repeat the check. (Generally, it is recommended to do vacuum before the check, but for the testing purpose I'd recommend turning it off to check the detection of visible-invisible-visible duplicates scenarios)

You're going to have to "couple" buffer locks in the style of
_bt_check_unique() (as well as keeping a buffer lock on "the first
leaf page a duplicate might be on" throughout) if you need the test to
work reliably.

The amcheck feature has no lock coupling at its "first key on the next page"
check. I think that's fine, because amcheck takes one snapshot at the
beginning and looks for pairs of visible-to-that-snapshot heap tuples with the
same scan key. _bt_check_unique(), unlike amcheck, must catch concurrent
inserts. If amcheck "checkunique" wanted to detect duplicates that would
appear when all transactions commit, it would need lock coupling. (I'm not
suggesting it do that.) Do you see a problem with the lack of lock coupling
at "first key on the next page"?

But why bother with that? The tool doesn't have to be
100% perfect at detecting corruption (nothing can be), and it's rather
unlikely that it will matter for this test. A simple test that doesn't
handle cross-page duplicates is still going to be very effective.

I agree, but perhaps the "first key on the next page" code is more complex
than general-case code would be. If the lack of lock coupling is fine, then I
think memory context lifecycle is the only obstacle making index page
boundaries special. Are there factors beyond that? We already have
state->lowkey kept across pages via MemoryContextAlloc(). Similar lines of
code could preserve the scan key for checkunique, making the "first key on the
next page" code unnecessary.

==== 2. Raises runtime by 476% despite no dead tuples

I used the following to create a table larger than RAM, 17GB table and 10GB
index on a system with 12GB RAM:

\set count 500000000
begin;
set maintenance_work_mem = '1GB';
set client_min_messages = debug1; -- debug2 is per-block spam
create temp table t as select n from generate_series(1,:count) t(n);
create unique index t_idx on t(n);
\dt+ t
\di+ t_idx
create extension amcheck;
select bt_index_check('t_idx', heapallindexed => false, checkunique => false);
select bt_index_check('t_idx', heapallindexed => false, checkunique => true);

Adding checkunique raised runtime from 58s to 276s, because it checks
visibility for every heap tuple. It could do the heap fetch and visibility
check lazily, when the index yields two heap TIDs for one scan key. That
should give zero visibility checks for this particular test case, and it
doesn't add visibility checks to bloated-table cases. Pseudo-code:

/*---
* scan_key is the last uniqueness-relevant scan key observed as
* bt_check_level_from_leftmost() moves right to traverse the leaf level.
* Will be NULL if the next tuple can't be the second tuple of a
* uniqueness violation, because any of the following apply:
* - we're evaluating the first leaf tuple of the entire index
* - last scan key had anynullkeys (never forms a uniqueness violation w/
* any other scan key)
*/
scan_key = NULL;
/*
* scan_key_known_visible==true indicates that scan_key_heap_tid is the
* last _visible_ heap TID observed for scan_key. Otherwise,
* scan_key_heap_tid is the last heap TID observed for scan_key, and we've
* not yet checked its visibility.
*/
bool scan_key_known_visible;
scan_key_heap_tid;
foreach itup (leftmost_leaf_level_tup .. rightmost_leaf_level_tup) {
if (itup.anynullkeys)
scan_key = NULL;
else if (scan_key != NULL &&
_bt_compare(scan_key, itup.key) == 0 &&
(scan_key_known_visible ||
(scan_key_known_visible = visible(scan_key_heap_tid))))
{
if (visible(itup.tid))
elog(ERROR, "duplicate in unique index");
}
else
{
/*
* No prior uniqueness-relevant key, or key changed, or we just
* learned scan_key_heap_tid was invisible. Make itup the
* standard by which we judge future index tuples as we move
* right.
*/
scan_key = itup.key;
scan_key_known_visible = false;
scan_key_heap_tid = itup.tid;
}
}

In reply to: Noah Misch (#41)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Sun, Mar 24, 2024 at 10:03 PM Noah Misch <noah@leadboat.com> wrote:

You're going to have to "couple" buffer locks in the style of
_bt_check_unique() (as well as keeping a buffer lock on "the first
leaf page a duplicate might be on" throughout) if you need the test to
work reliably.

The amcheck feature has no lock coupling at its "first key on the next page"
check. I think that's fine, because amcheck takes one snapshot at the
beginning and looks for pairs of visible-to-that-snapshot heap tuples with the
same scan key. _bt_check_unique(), unlike amcheck, must catch concurrent
inserts. If amcheck "checkunique" wanted to detect duplicates that would
appear when all transactions commit, it would need lock coupling. (I'm not
suggesting it do that.) Do you see a problem with the lack of lock coupling
at "first key on the next page"?

Practically speaking, no, I see no problems.

I agree, but perhaps the "first key on the next page" code is more complex
than general-case code would be. If the lack of lock coupling is fine, then I
think memory context lifecycle is the only obstacle making index page
boundaries special. Are there factors beyond that?

I believe that my concern back in 2021 was that the general complexity
of cross-page checking was unlikely to be worth it. Note that
nbtsplitloc.c is *maximally* aggressive about avoiding split points
that fall within some group of duplicates, so with a unique index it
should be very rare.

Admittedly, I was probably thinking about the complexity of adding a
bunch of code just to be able to check uniqueness across page
boundaries. I did mention lock coupling by name, but that was more of
a catch-all term for the problems in this area.

We already have
state->lowkey kept across pages via MemoryContextAlloc(). Similar lines of
code could preserve the scan key for checkunique, making the "first key on the
next page" code unnecessary.

I suspect that I was overly focussed on the index structure itself
back when I made these remarks. I might not have considered that just
using an MVCC snapshot for the TIDs makes the whole process safe,
though that now seems quite obvious.

Separately, I now see that the committed patch just reuses the code
that has long been used to check that things are in the correct order
across page boundaries: this is the bt_right_page_check_scankey check,
which existed in the very earliest versions of amcheck. So while I
agree that we could just keep the original scan key (from the last
item on every leaf page), and then make the check at the start of the
next page instead (as opposed to making it at the end of the previous
leaf page, which is how it works now), it's not obvious that that
would be a good trade-off, all things considered.

It might still be a little better that way around, overall, but you're
not just talking about changing the recently committed checkunique
patch (I think). You're also talking about restructuring the long
established bt_right_page_check_scankey check (otherwise, what's the
point?). I'm not categorically opposed to that, but it's not as if
it'll allow you to throw out a bunch of code -- AFAICT that proposal
doesn't have that clear advantage going for it. The race condition
that is described at great length in bt_right_page_check_scankey isn't
ever going to be a problem for the recently committed checkunique
patch (as you more or less pointed out yourself), but obviously it is
still a concern for the cross-page order check.

In summary, the old bt_right_page_check_scankey check is strictly
concerned with the consistency of a physical data structure (the index
itself), whereas the new checkunique check makes sure that the logical
content of the database is consistent (the index, the heap, and all
associated transaction status metadata have to be consistent). That
means that the concerns that are described at length in
bt_right_page_check_scankey (nor anything like those concerns) don't
apply to the new checkunique check. We agree on all that, I think. But
it's less clear that that presents us with an opportunity to simplify
this patch.

Adding checkunique raised runtime from 58s to 276s, because it checks
visibility for every heap tuple. It could do the heap fetch and visibility
check lazily, when the index yields two heap TIDs for one scan key. That
should give zero visibility checks for this particular test case, and it
doesn't add visibility checks to bloated-table cases.

The added runtime that you report seems quite excessive to me. I'm
really surprised that the code doesn't manage to avoid visibility
checks in the absence of duplicates that might both have TIDs
considered visible. Lazy visibility checking seems almost essential,
and not just a nice-to-have optimization.

It seems like the implication of everything that you said about
refactoring/moving the check was that doing so would enable this
optimization (at least an implementation along the lines of your
pseudo code). If that was what you intended, then it's not obvious to
me why it is relevant. What, if anything, does it have to do with
making the new checkunique visibility checks happen lazily?

--
Peter Geoghegan

#43Noah Misch
noah@leadboat.com
In reply to: Peter Geoghegan (#42)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, Mar 25, 2024 at 12:03:10PM -0400, Peter Geoghegan wrote:

On Sun, Mar 24, 2024 at 10:03 PM Noah Misch <noah@leadboat.com> wrote:

Separately, I now see that the committed patch just reuses the code
that has long been used to check that things are in the correct order
across page boundaries: this is the bt_right_page_check_scankey check,
which existed in the very earliest versions of amcheck. So while I
agree that we could just keep the original scan key (from the last
item on every leaf page), and then make the check at the start of the
next page instead (as opposed to making it at the end of the previous
leaf page, which is how it works now), it's not obvious that that
would be a good trade-off, all things considered.

It might still be a little better that way around, overall, but you're
not just talking about changing the recently committed checkunique
patch (I think). You're also talking about restructuring the long
established bt_right_page_check_scankey check (otherwise, what's the
point?). I'm not categorically opposed to that, but it's not as if

I wasn't thinking about changing the pre-v17 bt_right_page_check_scankey()
code. I got interested in this area when I saw the interaction of the new
"first key on the next page" logic with bt_right_page_check_scankey(). The
patch made bt_right_page_check_scankey() pass back rightfirstoffset. The new
code then does palloc_btree_page() and PageGetItem() with that offset, which
bt_right_page_check_scankey() had already done. That smelled like a misplaced
distribution of responsibility. For a time, I suspected the new code should
move down into bt_right_page_check_scankey(). Then I transitioned to thinking
checkunique didn't need new code for the page boundary.

it'll allow you to throw out a bunch of code -- AFAICT that proposal
doesn't have that clear advantage going for it. The race condition
that is described at great length in bt_right_page_check_scankey isn't
ever going to be a problem for the recently committed checkunique
patch (as you more or less pointed out yourself), but obviously it is
still a concern for the cross-page order check.

In summary, the old bt_right_page_check_scankey check is strictly
concerned with the consistency of a physical data structure (the index
itself), whereas the new checkunique check makes sure that the logical
content of the database is consistent (the index, the heap, and all
associated transaction status metadata have to be consistent). That
means that the concerns that are described at length in
bt_right_page_check_scankey (nor anything like those concerns) don't
apply to the new checkunique check. We agree on all that, I think. But
it's less clear that that presents us with an opportunity to simplify
this patch.

See above for why I anticipated a simplification opportunity with respect to
new-in-v17 code. Still, it may not pan out.

Adding checkunique raised runtime from 58s to 276s, because it checks

Side note: my last email incorrectly described that as "raises runtime by
476%". It should have said "by 376%" or "by a factor of 4.76".

visibility for every heap tuple. It could do the heap fetch and visibility
check lazily, when the index yields two heap TIDs for one scan key. That
should give zero visibility checks for this particular test case, and it
doesn't add visibility checks to bloated-table cases.

It seems like the implication of everything that you said about
refactoring/moving the check was that doing so would enable this
optimization (at least an implementation along the lines of your
pseudo code). If that was what you intended, then it's not obvious to
me why it is relevant. What, if anything, does it have to do with
making the new checkunique visibility checks happen lazily?

Their connection is just being the two big-picture topics I found in
post-commit review. Decisions about the cross-page check are indeed separable
from decisions about lazy vs. eager visibility checks.

Thanks,
nm

In reply to: Noah Misch (#43)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, Mar 25, 2024 at 2:24 PM Noah Misch <noah@leadboat.com> wrote:

I wasn't thinking about changing the pre-v17 bt_right_page_check_scankey()
code. I got interested in this area when I saw the interaction of the new
"first key on the next page" logic with bt_right_page_check_scankey(). The
patch made bt_right_page_check_scankey() pass back rightfirstoffset. The new
code then does palloc_btree_page() and PageGetItem() with that offset, which
bt_right_page_check_scankey() had already done. That smelled like a misplaced
distribution of responsibility. For a time, I suspected the new code should
move down into bt_right_page_check_scankey(). Then I transitioned to thinking
checkunique didn't need new code for the page boundary.

Ah, I see. Somehow I missed this point when I recently took a fresh
look at the committed patch.

I did notice (I meant to point out) that I have concerns about this
part of the new uniqueness check code:

"
if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
break;
"

My concern here is with the !P_ISLEAF(topaque) test -- it shouldn't be
required. If the page in question isn't a leaf page, then the index
must be corrupt (or the page deletion recycle safety/drain technique
thing is buggy). The " !P_ISLEAF(topaque)" part of the check is either
superfluous or something that ought to be reported as corruption --
it's not a legal/expected state.

Separately, I dislike the way the target block changes within
bt_target_page_check(). The general idea behind verify_nbtree.c's
target block is that every block becomes the target exactly once, in a
clearly defined place. All corruption (in the index structure itself)
is formally considered to be a problem with that particular target
block. I want to be able to clearly distinguish between the target and
target's right sibling here, to explain my concerns, but they're kinda
both the target, so that's a lot harder than it should be. (Admittedly
directly blaming the target block has always been a little bit
arbitrary, at least in certain cases, but even there it provides
structure that makes things much easier to describe unambiguously.)

--
Peter Geoghegan

#45Noah Misch
noah@leadboat.com
In reply to: Peter Geoghegan (#44)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, Mar 29, 2024 at 02:17:08PM -0400, Peter Geoghegan wrote:

On Mon, Mar 25, 2024 at 2:24 PM Noah Misch <noah@leadboat.com> wrote:

I wasn't thinking about changing the pre-v17 bt_right_page_check_scankey()
code. I got interested in this area when I saw the interaction of the new
"first key on the next page" logic with bt_right_page_check_scankey(). The
patch made bt_right_page_check_scankey() pass back rightfirstoffset. The new
code then does palloc_btree_page() and PageGetItem() with that offset, which
bt_right_page_check_scankey() had already done. That smelled like a misplaced
distribution of responsibility. For a time, I suspected the new code should
move down into bt_right_page_check_scankey(). Then I transitioned to thinking
checkunique didn't need new code for the page boundary.

I did notice (I meant to point out) that I have concerns about this
part of the new uniqueness check code:

"
if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
break;
"

My concern here is with the !P_ISLEAF(topaque) test -- it shouldn't be
required. If the page in question isn't a leaf page, then the index
must be corrupt (or the page deletion recycle safety/drain technique
thing is buggy). The " !P_ISLEAF(topaque)" part of the check is either
superfluous or something that ought to be reported as corruption --
it's not a legal/expected state.

Good point.

Separately, I dislike the way the target block changes within
bt_target_page_check(). The general idea behind verify_nbtree.c's
target block is that every block becomes the target exactly once, in a
clearly defined place.

Agreed.

#46Peter Eisentraut
peter@eisentraut.org
In reply to: Alexander Korotkov (#39)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On 24.10.23 22:13, Alexander Korotkov wrote:

On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

I think, this patch was marked as "Waiting on Author", probably, by mistake. Since recent changes were done without any significant code changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

This patch seems useful to me. I went through the thread, it seems
that all the critics are addressed.

I've rebased this patch. Also, I've run perltidy for tests, split
long errmsg() into errmsg(), errdetail() and errhint(), and do other
minor enchantments.

I think this patch is ready to go. I'm going to push it if there are
no objections.

I just found the new pg_amcheck option --checkunique in PG17-to-be.
Could we rename this to --check-unique? Seems friendlier. Maybe also
rename the bt_index_check function argument to check_unique.

#47Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Peter Geoghegan (#44)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

I did notice (I meant to point out) that I have concerns about this

part of the new uniqueness check code:
"
if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
break;
"

My concern here is with the !P_ISLEAF(topaque) test -- it shouldn't be

required

I agree. But I didn't see the need to check uniqueness constraints
violations in internal pages. Furthermore, it doesn't mean only a violation
of constraint, but a major index corruption. I agree that checking and
reporting this type of corruption separately is a possible thing.

Separately, I dislike the way the target block changes within

bt_target_page_check(). The general idea behind verify_nbtree.c's
target block is that every block becomes the target exactly once, in a
clearly defined place. All corruption (in the index structure itself)
is formally considered to be a problem with that particular target
block. I want to be able to clearly distinguish between the target and
target's right sibling here, to explain my concerns, but they're kinda
both the target, so that's a lot harder than it should be. (Admittedly
directly blaming the target block has always been a little bit
arbitrary, at least in certain cases, but even there it provides
structure that makes things much easier to describe unambiguously.)

The possible way to load the target block only once is to get rid of the
cross-page uniqueness violation check. I introduced it to catch more
possible cases of uniqueness violations. Though they are expected to be
extremely rare, and anyway the algorithm doesn't get any warranty, just
does its best to catch what is possible. I don't object to this change.

Regards,
Pavel.

#48Alexander Korotkov
aekorotkov@gmail.com
In reply to: Pavel Borisov (#47)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Wed, Apr 17, 2024 at 6:41 PM Pavel Borisov <pashkin.elfe@gmail.com> wrote:

I did notice (I meant to point out) that I have concerns about this
part of the new uniqueness check code:
"
if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
break;
"

My concern here is with the !P_ISLEAF(topaque) test -- it shouldn't be
required

I agree. But I didn't see the need to check uniqueness constraints violations in internal pages. Furthermore, it doesn't mean only a violation of constraint, but a major index corruption. I agree that checking and reporting this type of corruption separately is a possible thing.

I think we could just throw an error in case of an unexpected internal
page. It doesn't seem reasonable to continue the check with this type
of corruption detected. If the tree linkage is corrupted we may enter
an endless loop or something.

Separately, I dislike the way the target block changes within
bt_target_page_check(). The general idea behind verify_nbtree.c's
target block is that every block becomes the target exactly once, in a
clearly defined place. All corruption (in the index structure itself)
is formally considered to be a problem with that particular target
block. I want to be able to clearly distinguish between the target and
target's right sibling here, to explain my concerns, but they're kinda
both the target, so that's a lot harder than it should be. (Admittedly
directly blaming the target block has always been a little bit
arbitrary, at least in certain cases, but even there it provides
structure that makes things much easier to describe unambiguously.)

The possible way to load the target block only once is to get rid of the cross-page uniqueness violation check. I introduced it to catch more possible cases of uniqueness violations. Though they are expected to be extremely rare, and anyway the algorithm doesn't get any warranty, just does its best to catch what is possible. I don't object to this change.

I think we could probably just avoid setting state->target during
cross-page check. Just save that into a local variable and pass as an
argument where needed.

Skipping the visibility checks for "only one tuple for scan key" case
looks like very valuable optimization [1].

I also think we should wrap lVis_* variables into struct. That would
make the way we pass them to functions more elegant.

Links.
1. /messages/by-id/20240325020323.fd.nmisch@google.com

------
Regards,
Alexander Korotkov

#49Alexander Korotkov
aekorotkov@gmail.com
In reply to: Peter Eisentraut (#46)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Wed, Apr 17, 2024 at 9:38 AM Peter Eisentraut <peter@eisentraut.org> wrote:

On 24.10.23 22:13, Alexander Korotkov wrote:

On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

I think, this patch was marked as "Waiting on Author", probably, by mistake. Since recent changes were done without any significant code changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

This patch seems useful to me. I went through the thread, it seems
that all the critics are addressed.

I've rebased this patch. Also, I've run perltidy for tests, split
long errmsg() into errmsg(), errdetail() and errhint(), and do other
minor enchantments.

I think this patch is ready to go. I'm going to push it if there are
no objections.

I just found the new pg_amcheck option --checkunique in PG17-to-be.
Could we rename this to --check-unique? Seems friendlier. Maybe also
rename the bt_index_check function argument to check_unique.

+1 from me
Let's do so if nobody objects.

------
Regards,
Alexander Korotkov

#50Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Alexander Korotkov (#49)
5 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, hackers!

On Wed, 24 Apr 2024 at 13:58, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Wed, Apr 17, 2024 at 9:38 AM Peter Eisentraut <peter@eisentraut.org>
wrote:

On 24.10.23 22:13, Alexander Korotkov wrote:

On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

I think, this patch was marked as "Waiting on Author", probably, by

mistake. Since recent changes were done without any significant code
changes and CF bot how happy again.

I'm going to move it to RfC, could I? If not, please tell why.

I restored the "Ready for Committer" state. I don't think it's a good
practice to change the state every time the patch has a slight
conflict or something. This is not helpful at all. Such things happen
quite regularly and typically are fixed in a couple of days.

This patch seems useful to me. I went through the thread, it seems
that all the critics are addressed.

I've rebased this patch. Also, I've run perltidy for tests, split
long errmsg() into errmsg(), errdetail() and errhint(), and do other
minor enchantments.

I think this patch is ready to go. I'm going to push it if there are
no objections.

I just found the new pg_amcheck option --checkunique in PG17-to-be.
Could we rename this to --check-unique? Seems friendlier. Maybe also
rename the bt_index_check function argument to check_unique.

+1 from me
Let's do so if nobody objects.

Thank you very much for your input in this thread!

See the patches based on the proposals in the attachment:

0001: Optimize speed by avoiding heap visibility checking for different
non-deduplicated index tuples as proposed by Noah Misch

Speed measurements on my laptop using the exact method recommended by Noah
upthread:
Current master branch: checkunique off: 144s, checkunique on: 419s
With patch 0001: checkunique off: 141s, checkunique on: 171s

0002: Use structure to store and transfer info about last visible heap
entry (code refactoring) as proposed by Alexander Korotkov

0003: Don't load rightpage into BtreeCheckState (code refactoring) as
proposed by Peter Geoghegan

Loading of right page for cross-page unique constraint check in the same
way as in bt_right_page_check_scankey()

0004: Report error when next page to a leaf is not a leaf as proposed by
Peter Geoghegan

I think it's a very improbable condition and this check might be not
necessary, but it's right and safe to break check and report error.

0005: Rename checkunique parameter to more user friendly as proposed by
Peter Eisentraut and Alexander Korotkov

Again many thanks for the useful proposals!

Regards,
Pavel Borisov,
Supabase

Attachments:

v1-0002-Amcheck-code-refactoring.patchapplication/octet-stream; name=v1-0002-Amcheck-code-refactoring.patchDownload
From 92a085925e84cd6c34c59861f2775e53a1048665 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Apr 2024 14:07:45 +0400
Subject: [PATCH v1 2/5] Amcheck: code refactoring

Use structure to store and transfer info about last visible heap entry
among the equal index/posting list entries.

Reported-by: Alexander Korotkov
Discussion: https://www.postgresql.org/message-id/CAPpHfdsVbB9ToriaB1UHuOKwjKxiZmTFQcEF%3DjuzzC_nby31uA%40mail.gmail.com
---
 contrib/amcheck/verify_nbtree.c | 112 ++++++++++++++------------------
 1 file changed, 49 insertions(+), 63 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index ae8012f15b..66f2e619a8 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -145,6 +145,15 @@ typedef struct BtreeLevel
 	bool		istruerootlevel;
 } BtreeLevel;
 
+/* Info for last visible entry for checking unique constraint */
+typedef struct lVisInfo
+{
+	ItemPointer tid; 	/* Heap tid */
+	BlockNumber block;	/* Index block */
+	OffsetNumber offset;	/* Offset on index block */
+	int i;			/* Number in posting list. (-1 for non-deduplicated) */
+} lVisInfo;
+
 PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
@@ -165,17 +174,13 @@ static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
 static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
-static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
-								BlockNumber block, OffsetNumber offset,
-								int posting, ItemPointer nexttid,
+static void bt_report_duplicate(BtreeCheckState *state, lVisInfo *lVis,
+								ItemPointer nexttid,
 								BlockNumber nblock, OffsetNumber noffset,
 								int nposting);
 static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 								  BlockNumber targetblock,
-								  OffsetNumber offset, int *lVis_i,
-								  ItemPointer *lVis_tid,
-								  OffsetNumber *lVis_offset,
-								  BlockNumber *lVis_block);
+								  OffsetNumber offset, lVisInfo *lVis);
 static void bt_target_page_check(BtreeCheckState *state);
 static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
 												OffsetNumber *rightfirstoffset);
@@ -997,8 +1002,7 @@ heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
  */
 static void
 bt_report_duplicate(BtreeCheckState *state,
-					ItemPointer tid, BlockNumber block, OffsetNumber offset,
-					int posting,
+					lVisInfo *lVis,
 					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
 					int nposting)
 {
@@ -1010,18 +1014,18 @@ bt_report_duplicate(BtreeCheckState *state,
 			   *pnposting = "";
 
 	htid = psprintf("tid=(%u,%u)",
-					ItemPointerGetBlockNumberNoCheck(tid),
-					ItemPointerGetOffsetNumberNoCheck(tid));
+					ItemPointerGetBlockNumberNoCheck(lVis->tid),
+					ItemPointerGetOffsetNumberNoCheck(lVis->tid));
 	nhtid = psprintf("tid=(%u,%u)",
 					 ItemPointerGetBlockNumberNoCheck(nexttid),
 					 ItemPointerGetOffsetNumberNoCheck(nexttid));
-	itid = psprintf("tid=(%u,%u)", block, offset);
+	itid = psprintf("tid=(%u,%u)", lVis->block, lVis->offset);
 
-	if (nblock != block || noffset != offset)
+	if (nblock != lVis->block || noffset != lVis->offset)
 		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
 
-	if (posting >= 0)
-		pposting = psprintf(" posting %u", posting);
+	if (lVis->i >= 0)
+		pposting = psprintf(" posting %u", lVis->i);
 
 	if (nposting >= 0)
 		pnposting = psprintf(" posting %u", nposting);
@@ -1038,9 +1042,7 @@ bt_report_duplicate(BtreeCheckState *state,
 /* Check if current nbtree leaf entry complies with UNIQUE constraint */
 static void
 bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
-					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i,
-					  ItemPointer *lVis_tid, OffsetNumber *lVis_offset,
-					  BlockNumber *lVis_block)
+					  BlockNumber targetblock, OffsetNumber offset, lVisInfo *lVis)
 {
 	ItemPointer tid;
 	bool		has_visible_entry = false;
@@ -1049,7 +1051,7 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 
 	/*
 	 * Current tuple has posting list. Report duplicate if TID of any posting
-	 * list entry is visible and lVis_tid is valid.
+	 * list entry is visible and lVis->tid is valid.
 	 */
 	if (BTreeTupleIsPosting(itup))
 	{
@@ -1059,11 +1061,10 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 			if (heap_entry_is_visible(state, tid))
 			{
 				has_visible_entry = true;
-				if (ItemPointerIsValid(*lVis_tid))
+				if (ItemPointerIsValid(lVis->tid))
 				{
 					bt_report_duplicate(state,
-										*lVis_tid, *lVis_block,
-										*lVis_offset, *lVis_i,
+										lVis,
 										tid, targetblock,
 										offset, i);
 				}
@@ -1073,13 +1074,13 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 				 * between the posting list entries of the first tuple on the
 				 * page after cross-page check.
 				 */
-				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+				if (lVis->block != targetblock && ItemPointerIsValid(lVis->tid))
 					return;
 
-				*lVis_i = i;
-				*lVis_tid = tid;
-				*lVis_offset = offset;
-				*lVis_block = targetblock;
+				lVis->i = i;
+				lVis->tid = tid;
+				lVis->offset = offset;
+				lVis->block = targetblock;
 			}
 		}
 	}
@@ -1095,37 +1096,36 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 		if (heap_entry_is_visible(state, tid))
 		{
 			has_visible_entry = true;
-			if (ItemPointerIsValid(*lVis_tid))
+			if (ItemPointerIsValid(lVis->tid))
 			{
 				bt_report_duplicate(state,
-									*lVis_tid, *lVis_block,
-									*lVis_offset, *lVis_i,
+									lVis,
 									tid, targetblock,
 									offset, -1);
 			}
-			*lVis_i = -1;
-			*lVis_tid = tid;
-			*lVis_offset = offset;
-			*lVis_block = targetblock;
+			lVis->i = -1;
+			lVis->tid = tid;
+			lVis->offset = offset;
+			lVis->block = targetblock;
 		}
 	}
 
-	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
-		*lVis_block != targetblock)
+	if (!has_visible_entry && lVis->block != InvalidBlockNumber &&
+		lVis->block != targetblock)
 	{
 		char	   *posting = "";
 
-		if (*lVis_i >= 0)
-			posting = psprintf(" posting %u", *lVis_i);
+		if (lVis->i >= 0)
+			posting = psprintf(" posting %u", lVis->i);
 		ereport(DEBUG1,
 				(errcode(ERRCODE_NO_DATA),
 				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) in index \"%s\"",
 						targetblock, offset,
 						RelationGetRelationName(state->rel)),
 				 errdetail("It doesn't have visible heap tids and key is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)).",
-						   *lVis_block, *lVis_offset, posting,
-						   ItemPointerGetBlockNumberNoCheck(*lVis_tid),
-						   ItemPointerGetOffsetNumberNoCheck(*lVis_tid)),
+						   lVis->block, lVis->offset, posting,
+						   ItemPointerGetBlockNumberNoCheck(lVis->tid),
+						   ItemPointerGetOffsetNumberNoCheck(lVis->tid)),
 				 errhint("VACUUM the table and repeat the check.")));
 	}
 }
@@ -1373,11 +1373,7 @@ bt_target_page_check(BtreeCheckState *state)
 	BTPageOpaque topaque;
 
 	/* last visible entry info for checking indexes with unique constraint */
-	int			lVis_i = -1;	/* the position of last visible item for
-								 * posting tuple. for non-posting tuple (-1) */
-	ItemPointer lVis_tid = NULL;
-	BlockNumber lVis_block = InvalidBlockNumber;
-	OffsetNumber lVis_offset = InvalidOffsetNumber;
+	lVisInfo 	lVis = {NULL, InvalidBlockNumber, InvalidOffsetNumber, -1};
 
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1776,11 +1772,9 @@ bt_target_page_check(BtreeCheckState *state)
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && !skey->anynullkeys &&
-			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis.tid)))
 		{
-			bt_entry_unique_check(state, itup, state->targetblock, offset,
-								  &lVis_i, &lVis_tid, &lVis_offset,
-								  &lVis_block);
+			bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			unique_checked = true;
 		}
 
@@ -1805,17 +1799,13 @@ bt_target_page_check(BtreeCheckState *state)
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
 			{
-				lVis_i = -1;
-				lVis_tid = NULL;
-				lVis_block = InvalidBlockNumber;
-				lVis_offset = InvalidOffsetNumber;
+				lVis = (lVisInfo) {NULL, InvalidBlockNumber, InvalidOffsetNumber, -1};
 			}
 			else if (!unique_checked)
 			{
-				bt_entry_unique_check(state, itup, state->targetblock, offset,
-									  &lVis_i, &lVis_tid, &lVis_offset,
-									  &lVis_block);
+				bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			}
+
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1901,9 +1891,7 @@ bt_target_page_check(BtreeCheckState *state)
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
 					if (!unique_checked)
-						bt_entry_unique_check(state, itup, state->targetblock, offset,
-											  &lVis_i, &lVis_tid, &lVis_offset,
-											  &lVis_block);
+						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
@@ -1918,9 +1906,7 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightfirstoffset);
 					itup = (IndexTuple) PageGetItem(state->target, itemid);
 
-					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
-										  &lVis_i, &lVis_tid, &lVis_offset,
-										  &lVis_block);
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
 				}
 			}
 		}
-- 
2.34.1

v1-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patchapplication/octet-stream; name=v1-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patchDownload
From 526409e76419b387d9d4a40e94881231f256fcc8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Apr 2024 14:20:45 +0400
Subject: [PATCH v1 3/5] Amcheck: Don't load rightpage into BtreeCheckState

For cross-page unique constraint check use a local variable in the
similar way as implemented in bt_right_page_check_scankey().

Reported-by: Peter Geoghegan
---
 contrib/amcheck/verify_nbtree.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 66f2e619a8..d6f70206db 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1890,23 +1890,27 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					Page	rightpage;
+
 					if (!unique_checked)
 						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
-					state->target = palloc_btree_page(state,
+					rightpage = palloc_btree_page(state,
 													  rightblock_number);
-					topaque = BTPageGetOpaque(state->target);
+					topaque = BTPageGetOpaque(rightpage);
 
 					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
 						break;
 
 					itemid = PageGetItemIdCareful(state, rightblock_number,
-												  state->target,
+												  rightpage,
 												  rightfirstoffset);
-					itup = (IndexTuple) PageGetItem(state->target, itemid);
+					itup = (IndexTuple) PageGetItem(rightpage, itemid);
 
 					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
+
+					pfree(rightpage);
 				}
 			}
 		}
-- 
2.34.1

v1-0004-Amcheck-Report-error-when-next-page-to-a-leaf-is-.patchapplication/octet-stream; name=v1-0004-Amcheck-Report-error-when-next-page-to-a-leaf-is-.patchDownload
From a2950357675198285dfdbc974346bce12dbd3c55 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Apr 2024 15:06:36 +0400
Subject: [PATCH v1 4/5] Amcheck: Report error when next page to a leaf is not
 a leaf

This is a very unlikely condition during checking unique constraint,
meaning that index connectivity is violated badly and we shouldn't
continue checking to avoid neverending loops etc. So it's better
to  honestly throw an error.

Reported-by: Peter Geoghegan
---
 contrib/amcheck/verify_nbtree.c | 23 ++++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index d6f70206db..dfc9ed769f 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1829,7 +1829,6 @@ bt_target_page_check(BtreeCheckState *state)
 		if (offset == max)
 		{
 			BTScanInsert rightkey;
-			BlockNumber rightblock_number;
 
 			/* first offset on a right index page (log only) */
 			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
@@ -1874,12 +1873,12 @@ bt_target_page_check(BtreeCheckState *state)
 			 * If index has unique constraint make sure that no more than one
 			 * found equal items is visible.
 			 */
-			rightblock_number = topaque->btpo_next;
 			if (state->checkunique && state->indexinfo->ii_Unique &&
-				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+				rightkey && P_ISLEAF(topaque) && !P_RIGHTMOST(topaque))
 			{
-				elog(DEBUG2, "check cross page unique condition");
+				BlockNumber rightblock_number = topaque->btpo_next;
 
+				elog(DEBUG2, "check cross page unique condition");
 				/*
 				 * Make _bt_compare compare only index keys without heap TIDs.
 				 * rightkey->scantid is modified destructively but it is ok
@@ -1900,9 +1899,19 @@ bt_target_page_check(BtreeCheckState *state)
 													  rightblock_number);
 					topaque = BTPageGetOpaque(rightpage);
 
-					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
-						break;
-
+					if (P_IGNORE(topaque))
+					{
+						if (unlikely(!P_ISLEAF(topaque)))
+							ereport(ERROR,
+								(errcode(ERRCODE_INDEX_CORRUPTED),
+								errmsg("right block of leaf block is non-leaf for index \"%s\"",
+								RelationGetRelationName(state->rel)),
+								errdetail_internal("Block=%u page lsn=%X/%X.",
+								state->targetblock,
+								LSN_FORMAT_ARGS(state->targetlsn))));
+						else
+							break;
+					}
 					itemid = PageGetItemIdCareful(state, rightblock_number,
 												  rightpage,
 												  rightfirstoffset);
-- 
2.34.1

v1-0001-Amcheck-optimize-speed-of-checking-unique-constra.patchapplication/octet-stream; name=v1-0001-Amcheck-optimize-speed-of-checking-unique-constra.patchDownload
From 2847e83ef91bfa3e3ebda6a8acd60bd835950716 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Apr 2024 13:08:39 +0400
Subject: [PATCH v1 1/5] Amcheck: optimize speed of checking unique constraint

Check heap visibility only for non-equal non-deduplicated index
tuples. For deduplicated tuples we still need to check visibility
of all posting list entries because they represent equal
key values.

Reported-by: Noah Misch
---
 contrib/amcheck/verify_nbtree.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 20da4a46ba..ae8012f15b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1428,6 +1428,7 @@ bt_target_page_check(BtreeCheckState *state)
 		BTScanInsert skey;
 		bool		lowersizelimit;
 		ItemPointer scantid;
+		bool		unique_checked = false;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -1774,10 +1775,14 @@ bt_target_page_check(BtreeCheckState *state)
 		 * heap tuples visibility.
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
-			P_ISLEAF(topaque) && !skey->anynullkeys)
+			P_ISLEAF(topaque) && !skey->anynullkeys &&
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+		{
 			bt_entry_unique_check(state, itup, state->targetblock, offset,
 								  &lVis_i, &lVis_tid, &lVis_offset,
 								  &lVis_block);
+			unique_checked = true;
+		}
 
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
@@ -1805,6 +1810,12 @@ bt_target_page_check(BtreeCheckState *state)
 				lVis_block = InvalidBlockNumber;
 				lVis_offset = InvalidOffsetNumber;
 			}
+			else if (!unique_checked)
+			{
+				bt_entry_unique_check(state, itup, state->targetblock, offset,
+									  &lVis_i, &lVis_tid, &lVis_offset,
+									  &lVis_block);
+			}
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1889,6 +1900,11 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					if (!unique_checked)
+						bt_entry_unique_check(state, itup, state->targetblock, offset,
+											  &lVis_i, &lVis_tid, &lVis_offset,
+											  &lVis_block);
+
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
 													  rightblock_number);
-- 
2.34.1

v1-0005-Rename-checkunique-parameter-for-amcheck-and-pg_a.patchapplication/octet-stream; name=v1-0005-Rename-checkunique-parameter-for-amcheck-and-pg_a.patchDownload
From ff4d989173084431914220d1a66646a332fa9d71 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Apr 2024 15:36:38 +0400
Subject: [PATCH v1 5/5] Rename checkunique parameter for amcheck and
 pg_amcheck

Use more user friendly naming: --check-unique for pg_amcheck
command line, check_unique for amcheck sql functions

Reported-by: Peter Eisentraut
---
 contrib/amcheck/amcheck--1.3--1.4.sql      |  4 +--
 contrib/amcheck/expected/check_btree.out   | 12 +++----
 contrib/amcheck/sql/check_btree.sql        | 12 +++----
 contrib/amcheck/verify_nbtree.c            | 38 +++++++++++-----------
 doc/src/sgml/amcheck.sgml                  |  8 ++---
 doc/src/sgml/ref/pg_amcheck.sgml           |  4 +--
 src/bin/pg_amcheck/pg_amcheck.c            | 20 ++++++------
 src/bin/pg_amcheck/t/003_check.pl          | 20 ++++++------
 src/bin/pg_amcheck/t/005_opclass_damage.pl |  4 +--
 9 files changed, 61 insertions(+), 61 deletions(-)

diff --git a/contrib/amcheck/amcheck--1.3--1.4.sql b/contrib/amcheck/amcheck--1.3--1.4.sql
index 75574eaa64..e0d4f92085 100644
--- a/contrib/amcheck/amcheck--1.3--1.4.sql
+++ b/contrib/amcheck/amcheck--1.3--1.4.sql
@@ -11,7 +11,7 @@
 -- bt_index_parent_check()
 --
 CREATE FUNCTION bt_index_parent_check(index regclass,
-    heapallindexed boolean, rootdescend boolean, checkunique boolean)
+    heapallindexed boolean, rootdescend boolean, check_unique boolean)
 RETURNS VOID
 AS 'MODULE_PATHNAME', 'bt_index_parent_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
@@ -19,7 +19,7 @@ LANGUAGE C STRICT PARALLEL RESTRICTED;
 -- bt_index_check()
 --
 CREATE FUNCTION bt_index_check(index regclass,
-    heapallindexed boolean, checkunique boolean)
+    heapallindexed boolean, check_unique boolean)
 RETURNS VOID
 AS 'MODULE_PATHNAME', 'bt_index_check'
 LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out
index e7fb5f5515..ebb91d5d78 100644
--- a/contrib/amcheck/expected/check_btree.out
+++ b/contrib/amcheck/expected/check_btree.out
@@ -200,25 +200,25 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
 (1 row)
 
 -- UNIQUE constraint check
-SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, check_unique => true);
  bt_index_check 
 ----------------
  
 (1 row)
 
-SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, check_unique => true);
  bt_index_check 
 ----------------
  
 (1 row)
 
-SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, check_unique => true);
  bt_index_parent_check 
 -----------------------
  
 (1 row)
 
-SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, check_unique => true);
  bt_index_parent_check 
 -----------------------
  
@@ -227,14 +227,14 @@ SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend
 -- Check that null values in an unique index are not treated as equal
 CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
 INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
-SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, check_unique => true);
  bt_index_check 
 ----------------
  
 (1 row)
 
 CREATE INDEX on bttest_unique_nulls (b,c);
-SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, check_unique => true);
  bt_index_check 
 ----------------
  
diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql
index 0793dbfeeb..f9ae33a6a4 100644
--- a/contrib/amcheck/sql/check_btree.sql
+++ b/contrib/amcheck/sql/check_btree.sql
@@ -136,17 +136,17 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
 SELECT bt_index_check('bttest_a_expr_idx', true);
 
 -- UNIQUE constraint check
-SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
-SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
-SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
-SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
+SELECT bt_index_check('bttest_a_idx', heapallindexed => true, check_unique => true);
+SELECT bt_index_check('bttest_b_idx', heapallindexed => false, check_unique => true);
+SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, check_unique => true);
+SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, check_unique => true);
 
 -- Check that null values in an unique index are not treated as equal
 CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
 INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
-SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, check_unique => true);
 CREATE INDEX on bttest_unique_nulls (b,c);
-SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
+SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, check_unique => true);
 
 -- Check support of both 1B and 4B header sizes of short varlena datum
 CREATE TABLE varlena_bug (v text);
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index dfc9ed769f..3b673bac95 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -83,7 +83,7 @@ typedef struct BtreeCheckState
 	/* Also making sure non-pivot tuples can be found by new search? */
 	bool		rootdescend;
 	/* Also check uniqueness constraint if index is unique */
-	bool		checkunique;
+	bool		check_unique;
 	/* Per-page context */
 	MemoryContext targetcontext;
 	/* Buffer access strategy */
@@ -159,12 +159,12 @@ PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
 static void bt_index_check_internal(Oid indrelid, bool parentcheck,
 									bool heapallindexed, bool rootdescend,
-									bool checkunique);
+									bool check_unique);
 static inline void btree_index_checkable(Relation rel);
 static inline bool btree_index_mainfork_expected(Relation rel);
 static void bt_check_every_level(Relation rel, Relation heaprel,
 								 bool heapkeyspace, bool readonly, bool heapallindexed,
-								 bool rootdescend, bool checkunique);
+								 bool rootdescend, bool check_unique);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
 											   BtreeLevel level);
 static bool bt_leftmost_ignoring_half_dead(BtreeCheckState *state,
@@ -223,7 +223,7 @@ static inline ItemPointer BTreeTupleGetHeapTIDCareful(BtreeCheckState *state,
 static inline ItemPointer BTreeTupleGetPointsToTID(IndexTuple itup);
 
 /*
- * bt_index_check(index regclass, heapallindexed boolean, checkunique boolean)
+ * bt_index_check(index regclass, heapallindexed boolean, check_unique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -236,20 +236,20 @@ bt_index_check(PG_FUNCTION_ARGS)
 {
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
-	bool		checkunique = false;
+	bool		check_unique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
 	if (PG_NARGS() == 3)
-		checkunique = PG_GETARG_BOOL(2);
+		check_unique = PG_GETARG_BOOL(2);
 
-	bt_index_check_internal(indrelid, false, heapallindexed, false, checkunique);
+	bt_index_check_internal(indrelid, false, heapallindexed, false, check_unique);
 
 	PG_RETURN_VOID();
 }
 
 /*
- * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean)
+ * bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, check_unique boolean)
  *
  * Verify integrity of B-Tree index.
  *
@@ -263,16 +263,16 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
 	Oid			indrelid = PG_GETARG_OID(0);
 	bool		heapallindexed = false;
 	bool		rootdescend = false;
-	bool		checkunique = false;
+	bool		check_unique = false;
 
 	if (PG_NARGS() >= 2)
 		heapallindexed = PG_GETARG_BOOL(1);
 	if (PG_NARGS() >= 3)
 		rootdescend = PG_GETARG_BOOL(2);
 	if (PG_NARGS() == 4)
-		checkunique = PG_GETARG_BOOL(3);
+		check_unique = PG_GETARG_BOOL(3);
 
-	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, checkunique);
+	bt_index_check_internal(indrelid, true, heapallindexed, rootdescend, check_unique);
 
 	PG_RETURN_VOID();
 }
@@ -282,7 +282,7 @@ bt_index_parent_check(PG_FUNCTION_ARGS)
  */
 static void
 bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
-						bool rootdescend, bool checkunique)
+						bool rootdescend, bool check_unique)
 {
 	Oid			heapid;
 	Relation	indrel;
@@ -394,7 +394,7 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
 
 		/* Check index, possibly against table it is an index on */
 		bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
-							 heapallindexed, rootdescend, checkunique);
+							 heapallindexed, rootdescend, check_unique);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -496,7 +496,7 @@ btree_index_mainfork_expected(Relation rel)
 static void
 bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 					 bool readonly, bool heapallindexed, bool rootdescend,
-					 bool checkunique)
+					 bool check_unique)
 {
 	BtreeCheckState *state;
 	Page		metapage;
@@ -528,7 +528,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->readonly = readonly;
 	state->heapallindexed = heapallindexed;
 	state->rootdescend = rootdescend;
-	state->checkunique = checkunique;
+	state->check_unique = check_unique;
 	state->snapshot = InvalidSnapshot;
 
 	if (state->heapallindexed)
@@ -592,7 +592,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	 * performance take it once per index check. If snapshot already taken
 	 * reuse it.
 	 */
-	if (state->checkunique)
+	if (state->check_unique)
 	{
 		state->indexinfo = BuildIndexInfo(state->rel);
 		if (state->indexinfo->ii_Unique)
@@ -1770,7 +1770,7 @@ bt_target_page_check(BtreeCheckState *state)
 		 * If the index is unique verify entries uniqueness by checking the
 		 * heap tuples visibility.
 		 */
-		if (state->checkunique && state->indexinfo->ii_Unique &&
+		if (state->check_unique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && !skey->anynullkeys &&
 			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis.tid)))
 		{
@@ -1778,7 +1778,7 @@ bt_target_page_check(BtreeCheckState *state)
 			unique_checked = true;
 		}
 
-		if (state->checkunique && state->indexinfo->ii_Unique &&
+		if (state->check_unique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
 		{
 			/* Save current scankey tid */
@@ -1873,7 +1873,7 @@ bt_target_page_check(BtreeCheckState *state)
 			 * If index has unique constraint make sure that no more than one
 			 * found equal items is visible.
 			 */
-			if (state->checkunique && state->indexinfo->ii_Unique &&
+			if (state->check_unique && state->indexinfo->ii_Unique &&
 				rightkey && P_ISLEAF(topaque) && !P_RIGHTMOST(topaque))
 			{
 				BlockNumber rightblock_number = topaque->btpo_next;
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index 3af065615b..3464ab5e76 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -61,7 +61,7 @@
   <variablelist>
    <varlistentry>
     <term>
-     <function>bt_index_check(index regclass, heapallindexed boolean, checkunique boolean) returns void</function>
+     <function>bt_index_check(index regclass, heapallindexed boolean, check_unique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_check</primary>
      </indexterm>
@@ -118,7 +118,7 @@ ORDER BY c.relpages DESC LIMIT 10;
       that span child/parent relationships, but will verify the
       presence of all heap tuples as index tuples within the index
       when <parameter>heapallindexed</parameter> is
-      <literal>true</literal>.  When <parameter>checkunique</parameter>
+      <literal>true</literal>.  When <parameter>check_unique</parameter>
       is <literal>true</literal> <function>bt_index_check</function> will
       check that no more than one among duplicate entries in unique
       index is visible.  When a routine, lightweight test for
@@ -132,7 +132,7 @@ ORDER BY c.relpages DESC LIMIT 10;
 
    <varlistentry>
     <term>
-     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean) returns void</function>
+     <function>bt_index_parent_check(index regclass, heapallindexed boolean, rootdescend boolean, check_unique boolean) returns void</function>
      <indexterm>
       <primary>bt_index_parent_check</primary>
      </indexterm>
@@ -145,7 +145,7 @@ ORDER BY c.relpages DESC LIMIT 10;
       Optionally, when the <parameter>heapallindexed</parameter>
       argument is <literal>true</literal>, the function verifies the
       presence of all heap tuples that should be found within the
-      index.  When <parameter>checkunique</parameter>
+      index.  When <parameter>check_unique</parameter>
       is <literal>true</literal> <function>bt_index_parent_check</function> will
       check that no more than one among duplicate entries in unique
       index is visible.  When the optional <parameter>rootdescend</parameter>
diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml
index 067c806b46..0837045632 100644
--- a/doc/src/sgml/ref/pg_amcheck.sgml
+++ b/doc/src/sgml/ref/pg_amcheck.sgml
@@ -434,12 +434,12 @@ PostgreSQL documentation
     </varlistentry>
 
     <varlistentry>
-     <term><option>--checkunique</option></term>
+     <term><option>--check-unique</option></term>
      <listitem>
       <para>
        For each index with unique constraint checked, verify that no more than
        one among duplicate entries is visible in the index using <xref linkend="amcheck"/>'s
-       <option>checkunique</option> option.
+       <option>check-unique</option> option.
       </para>
      </listitem>
     </varlistentry>
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 7e3101704d..3aaf69a3b4 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -102,7 +102,7 @@ typedef struct AmcheckOptions
 	bool		parent_check;
 	bool		rootdescend;
 	bool		heapallindexed;
-	bool		checkunique;
+	bool		check_unique;
 
 	/* heap and btree hybrid option */
 	bool		no_btree_expansion;
@@ -133,7 +133,7 @@ static AmcheckOptions opts = {
 	.parent_check = false,
 	.rootdescend = false,
 	.heapallindexed = false,
-	.checkunique = false,
+	.check_unique = false,
 	.no_btree_expansion = false
 };
 
@@ -270,7 +270,7 @@ main(int argc, char *argv[])
 		{"heapallindexed", no_argument, NULL, 11},
 		{"parent-check", no_argument, NULL, 12},
 		{"install-missing", optional_argument, NULL, 13},
-		{"checkunique", no_argument, NULL, 14},
+		{"check-unique", no_argument, NULL, 14},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -439,7 +439,7 @@ main(int argc, char *argv[])
 					opts.install_schema = pg_strdup(optarg);
 				break;
 			case 14:
-				opts.checkunique = true;
+				opts.check_unique = true;
 				break;
 			default:
 				/* getopt_long already emitted a complaint */
@@ -602,7 +602,7 @@ main(int argc, char *argv[])
 		 * constraint check with warning if it is not yet supported by
 		 * amcheck.
 		 */
-		if (opts.checkunique == true)
+		if (opts.check_unique == true)
 		{
 			/*
 			 * Now amcheck has only major and minor versions in the string but
@@ -617,11 +617,11 @@ main(int argc, char *argv[])
 			sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev);
 
 			/*
-			 * checkunique option is supported in amcheck since version 1.4
+			 * check_unique option is supported in amcheck since version 1.4
 			 */
 			if ((vmaj == 1 && vmin < 4) || vmaj == 0)
 			{
-				pg_log_warning("--checkunique option is not supported by amcheck "
+				pg_log_warning("--check-unique option is not supported by amcheck "
 							   "version \"%s\"", amcheck_version);
 				dat->is_checkunique = false;
 			}
@@ -895,7 +895,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
 						  (opts.rootdescend ? "true" : "false"),
-						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
+						  (rel->datinfo->is_checkunique ? ", check_unique := true" : ""),
 						  rel->reloid);
 	else
 		appendPQExpBuffer(sql,
@@ -909,7 +909,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 						  "AND i.indisready AND i.indisvalid AND i.indislive",
 						  rel->datinfo->amcheck_schema,
 						  (opts.heapallindexed ? "true" : "false"),
-						  (rel->datinfo->is_checkunique ? ", checkunique := true" : ""),
+						  (rel->datinfo->is_checkunique ? ", check_unique := true" : ""),
 						  rel->reloid);
 }
 
@@ -1208,7 +1208,7 @@ help(const char *progname)
 	printf(_("      --heapallindexed            check that all heap tuples are found within indexes\n"));
 	printf(_("      --parent-check              check index parent/child relationships\n"));
 	printf(_("      --rootdescend               search from root page to refind tuples\n"));
-	printf(_("      --checkunique               check unique constraint if index is unique\n"));
+	printf(_("      --check-unique               check unique constraint if index is unique\n"));
 	printf(_("\nConnection options:\n"));
 	printf(_("  -h, --host=HOSTNAME             database server host or socket directory\n"));
 	printf(_("  -p, --port=PORT                 database server port\n"));
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 4b16bda6a4..0a8cf8bced 100644
--- a/src/bin/pg_amcheck/t/003_check.pl
+++ b/src/bin/pg_amcheck/t/003_check.pl
@@ -523,35 +523,35 @@ $node->command_checks_all(
 $node->command_checks_all(
 	[
 		@cmd, '-s', 's1', '-i', 't1_btree', '--parent-check',
-		'--checkunique', 'db1'
+		'--check-unique', 'db1'
 	],
 	2,
 	[$index_missing_relation_fork_re],
 	[$no_output_re],
-	'pg_amcheck smoke test --parent-check --checkunique');
+	'pg_amcheck smoke test --parent-check --check-unique');
 
 $node->command_checks_all(
 	[
 		@cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed',
-		'--rootdescend', '--checkunique', 'db1'
+		'--rootdescend', '--check-unique', 'db1'
 	],
 	2,
 	[$index_missing_relation_fork_re],
 	[$no_output_re],
-	'pg_amcheck smoke test --heapallindexed --rootdescend --checkunique');
+	'pg_amcheck smoke test --heapallindexed --rootdescend --check-unique');
 
 $node->command_checks_all(
 	[
-		@cmd, '--checkunique', '-d', 'db1', '-d', 'db2',
+		@cmd, '--check-unique', '-d', 'db1', '-d', 'db2',
 		'-d', 'db3', '-S', 's*'
 	],
 	0,
 	[$no_output_re],
 	[$no_output_re],
-	'pg_amcheck excluding all corrupt schemas with --checkunique option');
+	'pg_amcheck excluding all corrupt schemas with --check-unique option');
 
 #
-# Smoke test for checkunique option for not supported versions.
+# Smoke test for check-unique option for not supported versions.
 #
 $node->safe_psql(
 	'db3', q(
@@ -560,11 +560,11 @@ $node->safe_psql(
 ));
 
 $node->command_checks_all(
-	[ @cmd, '--checkunique', 'db3' ],
+	[ @cmd, '--check-unique', 'db3' ],
 	0,
 	[$no_output_re],
 	[
-		qr/pg_amcheck: warning: --checkunique option is not supported by amcheck version "1.3"/
+		qr/pg_amcheck: warning: --check-unique option is not supported by amcheck version "1.3"/
 	],
-	'pg_amcheck smoke test --checkunique');
+	'pg_amcheck smoke test --check-unique');
 done_testing();
diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl
index 1eea215227..0db58dee5b 100644
--- a/src/bin/pg_amcheck/t/005_opclass_damage.pl
+++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl
@@ -90,7 +90,7 @@ $node->safe_psql(
 
 # We should get no corruptions
 $node->command_like(
-	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	[ 'pg_amcheck', '--check-unique', '-p', $node->port, 'postgres' ],
 	qr/^$/,
 	'pg_amcheck all schemas, tables and indexes reports no corruption');
 
@@ -116,7 +116,7 @@ $node->safe_psql(
 
 # Unique index corruption should now be reported
 $node->command_checks_all(
-	[ 'pg_amcheck', '--checkunique', '-p', $node->port, 'postgres' ],
+	[ 'pg_amcheck', '--check-unique', '-p', $node->port, 'postgres' ],
 	2,
 	[qr/index uniqueness is violated for index "bttest_unique_idx"/],
 	[],
-- 
2.34.1

#51Karina Litskevich
litskevichkarina@gmail.com
In reply to: Pavel Borisov (#50)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, hackers!

On Thu, Apr 25, 2024 at 4:00 PM Pavel Borisov <pashkin.elfe@gmail.com>
wrote:

0005: Rename checkunique parameter to more user friendly as proposed by
Peter Eisentraut and Alexander Korotkov

I'm not sure renaming checkunique is a good idea. Other arguments of
bt_index_check and bt_index_parent_check functions (heapallindexed and
rootdescend) don't have underscore character in them. Corresponding
pg_amcheck options (--heapallindexed and --rootdescend) are also written
in one piece. check_unique and --check-unique stand out. Making arguments
and options in different styles doesn't seem user friendly to me.

Best regards,
Karina Litskevich
Postgres Professional: http://postgrespro.com/

#52Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Karina Litskevich (#51)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Karina!

On Thu, 25 Apr 2024 at 17:44, Karina Litskevich <litskevichkarina@gmail.com>
wrote:

Hi, hackers!

On Thu, Apr 25, 2024 at 4:00 PM Pavel Borisov <pashkin.elfe@gmail.com>
wrote:

0005: Rename checkunique parameter to more user friendly as proposed by
Peter Eisentraut and Alexander Korotkov

I'm not sure renaming checkunique is a good idea. Other arguments of
bt_index_check and bt_index_parent_check functions (heapallindexed and
rootdescend) don't have underscore character in them. Corresponding
pg_amcheck options (--heapallindexed and --rootdescend) are also written
in one piece. check_unique and --check-unique stand out. Making arguments
and options in different styles doesn't seem user friendly to me.

I did it under the consensus of Peter Eisentraut and Alexander Korotkov.
The pro for renaming is more user-friendly naming, I also agree.
The cons is that we already have both styles: "non-user friendly"
heapallindexed and rootdescend and "user-friendly" parent-check.

I'm ready to go with consensus in this matter. It's also not yet too late
to make it unique-check (instead of check-unique) to be better in style
with parent-check.

Kind regards,
Pavel

#53Noah Misch
noah@leadboat.com
In reply to: Pavel Borisov (#50)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Thu, Apr 25, 2024 at 04:59:54PM +0400, Pavel Borisov wrote:

0001: Optimize speed by avoiding heap visibility checking for different
non-deduplicated index tuples as proposed by Noah Misch

Speed measurements on my laptop using the exact method recommended by Noah
upthread:
Current master branch: checkunique off: 144s, checkunique on: 419s
With patch 0001: checkunique off: 141s, checkunique on: 171s

Where is the CPU time going to make it still be 21% slower w/ checkunique on?
It's a great improvement vs. current master, but I don't have an obvious
explanation for the remaining +21%.

#54Alexander Korotkov
aekorotkov@gmail.com
In reply to: Noah Misch (#53)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi Noah,

On Wed, May 1, 2024 at 5:24 AM Noah Misch <noah@leadboat.com> wrote:

On Thu, Apr 25, 2024 at 04:59:54PM +0400, Pavel Borisov wrote:

0001: Optimize speed by avoiding heap visibility checking for different
non-deduplicated index tuples as proposed by Noah Misch

Speed measurements on my laptop using the exact method recommended by Noah
upthread:
Current master branch: checkunique off: 144s, checkunique on: 419s
With patch 0001: checkunique off: 141s, checkunique on: 171s

Where is the CPU time going to make it still be 21% slower w/ checkunique on?
It's a great improvement vs. current master, but I don't have an obvious
explanation for the remaining +21%.

I think there is at least extra index tuples comparison.

------
Regards,
Alexander Korotkov

#55Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#54)
4 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Wed, May 1, 2024 at 5:26 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Wed, May 1, 2024 at 5:24 AM Noah Misch <noah@leadboat.com> wrote:

On Thu, Apr 25, 2024 at 04:59:54PM +0400, Pavel Borisov wrote:

0001: Optimize speed by avoiding heap visibility checking for different
non-deduplicated index tuples as proposed by Noah Misch

Speed measurements on my laptop using the exact method recommended by Noah
upthread:
Current master branch: checkunique off: 144s, checkunique on: 419s
With patch 0001: checkunique off: 141s, checkunique on: 171s

Where is the CPU time going to make it still be 21% slower w/ checkunique on?
It's a great improvement vs. current master, but I don't have an obvious
explanation for the remaining +21%.

I think there is at least extra index tuples comparison.

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

I don't post the patch with rename of new option. It doesn't seem
there is a consensus. I must admit that keeping all the options in
the same naming convention makes sense.

------
Regards,
Alexander Korotkov
Supabase

Attachments:

v2-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patchapplication/octet-stream; name=v2-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patchDownload
From 50f38667510057beea9c88bff8b7ee8cda825939 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:08:15 +0300
Subject: [PATCH v2 3/4] Amcheck: Don't load rightpage into BtreeCheckState

For cross-page unique constraint check use a local variable in the
similar way as implemented in bt_right_page_check_scankey().

Reported-by: Peter Geoghegan
---
 contrib/amcheck/verify_nbtree.c | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b433cb33254..977f8b6799d 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1910,6 +1910,8 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					Page		rightpage;
+
 					/*
 					 * Do the bt_entry_unique_check() call if it was
 					 * postponed.
@@ -1918,19 +1920,21 @@ bt_target_page_check(BtreeCheckState *state)
 						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
-					state->target = palloc_btree_page(state,
-													  rightblock_number);
-					topaque = BTPageGetOpaque(state->target);
+					rightpage = palloc_btree_page(state,
+												  rightblock_number);
+					topaque = BTPageGetOpaque(rightpage);
 
 					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
 						break;
 
 					itemid = PageGetItemIdCareful(state, rightblock_number,
-												  state->target,
+												  rightpage,
 												  rightfirstoffset);
-					itup = (IndexTuple) PageGetItem(state->target, itemid);
+					itup = (IndexTuple) PageGetItem(rightpage, itemid);
 
 					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
+
+					pfree(rightpage);
 				}
 			}
 		}
-- 
2.39.3 (Apple Git-145)

v2-0002-amcheck-Refactoring-the-storage-of-the-last-visib.patchapplication/octet-stream; name=v2-0002-amcheck-Refactoring-the-storage-of-the-last-visib.patchDownload
From 9ae515266a041b30d14913000eb00dd9363fc1f8 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:08:07 +0300
Subject: [PATCH v2 2/4] amcheck: Refactoring the storage of the last visible
 entry

This commit introduces a new data structure BtreeLastVisibleEntry comprising
information about the last visible heap entry with the current value of key.
Usage of this data structure allows us to avoid passing all this information
as individual function arguments.

Reported-by: Alexander Korotkov
Discussion: https://www.postgresql.org/message-id/CAPpHfdsVbB9ToriaB1UHuOKwjKxiZmTFQcEF%3DjuzzC_nby31uA%40mail.gmail.com
Author: Pavel Borisov, Alexander Korotkov
---
 contrib/amcheck/verify_nbtree.c  | 125 +++++++++++++++----------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 61 insertions(+), 65 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index c7be785f88b..b433cb33254 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -145,6 +145,19 @@ typedef struct BtreeLevel
 	bool		istruerootlevel;
 } BtreeLevel;
 
+/*
+ * Information about the last visible entry with current B-tree key.  Used
+ * for validation of the unique constraint.
+ */
+typedef struct BtreeLastVisibleEntry
+{
+	BlockNumber blkno;			/* Index block */
+	OffsetNumber offset;		/* Offset on index block */
+	int			postingIndex;	/* Number in the posting list (-1 for
+								 * non-deduplicated tuples) */
+	ItemPointer tid;			/* Heap tid */
+} BtreeLastVisibleEntry;
+
 PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
@@ -165,17 +178,13 @@ static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
 static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
-static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
-								BlockNumber block, OffsetNumber offset,
-								int posting, ItemPointer nexttid,
+static void bt_report_duplicate(BtreeCheckState *state, BtreeLastVisibleEntry *lVis,
+								ItemPointer nexttid,
 								BlockNumber nblock, OffsetNumber noffset,
 								int nposting);
 static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 								  BlockNumber targetblock,
-								  OffsetNumber offset, int *lVis_i,
-								  ItemPointer *lVis_tid,
-								  OffsetNumber *lVis_offset,
-								  BlockNumber *lVis_block);
+								  OffsetNumber offset, BtreeLastVisibleEntry *lVis);
 static void bt_target_page_check(BtreeCheckState *state);
 static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
 												OffsetNumber *rightfirstoffset);
@@ -997,8 +1006,7 @@ heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
  */
 static void
 bt_report_duplicate(BtreeCheckState *state,
-					ItemPointer tid, BlockNumber block, OffsetNumber offset,
-					int posting,
+					BtreeLastVisibleEntry *lVis,
 					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
 					int nposting)
 {
@@ -1010,18 +1018,18 @@ bt_report_duplicate(BtreeCheckState *state,
 			   *pnposting = "";
 
 	htid = psprintf("tid=(%u,%u)",
-					ItemPointerGetBlockNumberNoCheck(tid),
-					ItemPointerGetOffsetNumberNoCheck(tid));
+					ItemPointerGetBlockNumberNoCheck(lVis->tid),
+					ItemPointerGetOffsetNumberNoCheck(lVis->tid));
 	nhtid = psprintf("tid=(%u,%u)",
 					 ItemPointerGetBlockNumberNoCheck(nexttid),
 					 ItemPointerGetOffsetNumberNoCheck(nexttid));
-	itid = psprintf("tid=(%u,%u)", block, offset);
+	itid = psprintf("tid=(%u,%u)", lVis->blkno, lVis->offset);
 
-	if (nblock != block || noffset != offset)
+	if (nblock != lVis->blkno || noffset != lVis->offset)
 		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
 
-	if (posting >= 0)
-		pposting = psprintf(" posting %u", posting);
+	if (lVis->postingIndex >= 0)
+		pposting = psprintf(" posting %u", lVis->postingIndex);
 
 	if (nposting >= 0)
 		pnposting = psprintf(" posting %u", nposting);
@@ -1038,9 +1046,7 @@ bt_report_duplicate(BtreeCheckState *state,
 /* Check if current nbtree leaf entry complies with UNIQUE constraint */
 static void
 bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
-					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i,
-					  ItemPointer *lVis_tid, OffsetNumber *lVis_offset,
-					  BlockNumber *lVis_block)
+					  BlockNumber targetblock, OffsetNumber offset, BtreeLastVisibleEntry *lVis)
 {
 	ItemPointer tid;
 	bool		has_visible_entry = false;
@@ -1049,7 +1055,7 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 
 	/*
 	 * Current tuple has posting list. Report duplicate if TID of any posting
-	 * list entry is visible and lVis_tid is valid.
+	 * list entry is visible and lVis->tid is valid.
 	 */
 	if (BTreeTupleIsPosting(itup))
 	{
@@ -1059,11 +1065,10 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 			if (heap_entry_is_visible(state, tid))
 			{
 				has_visible_entry = true;
-				if (ItemPointerIsValid(*lVis_tid))
+				if (ItemPointerIsValid(lVis->tid))
 				{
 					bt_report_duplicate(state,
-										*lVis_tid, *lVis_block,
-										*lVis_offset, *lVis_i,
+										lVis,
 										tid, targetblock,
 										offset, i);
 				}
@@ -1073,13 +1078,13 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 				 * between the posting list entries of the first tuple on the
 				 * page after cross-page check.
 				 */
-				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+				if (lVis->blkno != targetblock && ItemPointerIsValid(lVis->tid))
 					return;
 
-				*lVis_i = i;
-				*lVis_tid = tid;
-				*lVis_offset = offset;
-				*lVis_block = targetblock;
+				lVis->blkno = targetblock;
+				lVis->offset = offset;
+				lVis->postingIndex = i;
+				lVis->tid = tid;
 			}
 		}
 	}
@@ -1087,7 +1092,7 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 	/*
 	 * Current tuple has no posting list. If TID is visible save info about it
 	 * for the next comparisons in the loop in bt_target_page_check(). Report
-	 * duplicate if lVis_tid is already valid.
+	 * duplicate if lVis->tid is already valid.
 	 */
 	else
 	{
@@ -1095,37 +1100,38 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 		if (heap_entry_is_visible(state, tid))
 		{
 			has_visible_entry = true;
-			if (ItemPointerIsValid(*lVis_tid))
+			if (ItemPointerIsValid(lVis->tid))
 			{
 				bt_report_duplicate(state,
-									*lVis_tid, *lVis_block,
-									*lVis_offset, *lVis_i,
+									lVis,
 									tid, targetblock,
 									offset, -1);
 			}
-			*lVis_i = -1;
-			*lVis_tid = tid;
-			*lVis_offset = offset;
-			*lVis_block = targetblock;
+
+			lVis->blkno = targetblock;
+			lVis->offset = offset;
+			lVis->tid = tid;
+			lVis->postingIndex = -1;
 		}
 	}
 
-	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
-		*lVis_block != targetblock)
+	if (!has_visible_entry &&
+		lVis->blkno != InvalidBlockNumber &&
+		lVis->blkno != targetblock)
 	{
 		char	   *posting = "";
 
-		if (*lVis_i >= 0)
-			posting = psprintf(" posting %u", *lVis_i);
+		if (lVis->postingIndex >= 0)
+			posting = psprintf(" posting %u", lVis->postingIndex);
 		ereport(DEBUG1,
 				(errcode(ERRCODE_NO_DATA),
 				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) in index \"%s\"",
 						targetblock, offset,
 						RelationGetRelationName(state->rel)),
 				 errdetail("It doesn't have visible heap tids and key is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)).",
-						   *lVis_block, *lVis_offset, posting,
-						   ItemPointerGetBlockNumberNoCheck(*lVis_tid),
-						   ItemPointerGetOffsetNumberNoCheck(*lVis_tid)),
+						   lVis->blkno, lVis->offset, posting,
+						   ItemPointerGetBlockNumberNoCheck(lVis->tid),
+						   ItemPointerGetOffsetNumberNoCheck(lVis->tid)),
 				 errhint("VACUUM the table and repeat the check.")));
 	}
 }
@@ -1372,12 +1378,8 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
-	/* last visible entry info for checking indexes with unique constraint */
-	int			lVis_i = -1;	/* the position of last visible item for
-								 * posting tuple. for non-posting tuple (-1) */
-	ItemPointer lVis_tid = NULL;
-	BlockNumber lVis_block = InvalidBlockNumber;
-	OffsetNumber lVis_offset = InvalidOffsetNumber;
+	/* Last visible entry info for checking indexes with unique constraint */
+	BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL};
 
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1784,11 +1786,9 @@ bt_target_page_check(BtreeCheckState *state)
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && !skey->anynullkeys &&
-			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis.tid)))
 		{
-			bt_entry_unique_check(state, itup, state->targetblock, offset,
-								  &lVis_i, &lVis_tid, &lVis_offset,
-								  &lVis_block);
+			bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			unique_checked = true;
 		}
 
@@ -1816,17 +1816,16 @@ bt_target_page_check(BtreeCheckState *state)
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
 			{
-				lVis_i = -1;
-				lVis_tid = NULL;
-				lVis_block = InvalidBlockNumber;
-				lVis_offset = InvalidOffsetNumber;
+				lVis.blkno = InvalidBlockNumber;
+				lVis.offset = InvalidOffsetNumber;
+				lVis.postingIndex = -1;
+				lVis.tid = NULL;
 			}
 			else if (!unique_checked)
 			{
-				bt_entry_unique_check(state, itup, state->targetblock, offset,
-									  &lVis_i, &lVis_tid, &lVis_offset,
-									  &lVis_block);
+				bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			}
+
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1916,9 +1915,7 @@ bt_target_page_check(BtreeCheckState *state)
 					 * postponed.
 					 */
 					if (!unique_checked)
-						bt_entry_unique_check(state, itup, state->targetblock, offset,
-											  &lVis_i, &lVis_tid, &lVis_offset,
-											  &lVis_block);
+						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
@@ -1933,9 +1930,7 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightfirstoffset);
 					itup = (IndexTuple) PageGetItem(state->target, itemid);
 
-					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
-										  &lVis_i, &lVis_tid, &lVis_offset,
-										  &lVis_block);
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
 				}
 			}
 		}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2311f82d81e..7535dc6d692 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -315,6 +315,7 @@ BrinStatsData
 BrinTuple
 BrinValues
 BtreeCheckState
+BtreeLastVisibleEntry
 BtreeLevel
 Bucket
 BufFile
-- 
2.39.3 (Apple Git-145)

v2-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchapplication/octet-stream; name=v2-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchDownload
From 0c8a911657e68f77255f849897319c227160bd02 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:07:53 +0300
Subject: [PATCH v2 1/4] amcheck: Optimize speed of checking for unique
 constraint violation

Currently, when amcheck validates a unique constraint, it visits the heap for
each index tuple.  This commit implements skipping keys, which have only one
non-dedeuplicated index tuple (quite common case for unique indexes). That
gives substantial economy on index checking time.

Reported-by: Noah Misch
Discussion: https://postgr.es/m/20240325020323.fd.nmisch%40google.com
Author: Alexander Korotkov, Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 35 +++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 70f65b645a6..c7be785f88b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1429,6 +1429,13 @@ bt_target_page_check(BtreeCheckState *state)
 		bool		lowersizelimit;
 		ItemPointer scantid;
 
+		/*
+		 * True if we already called bt_entry_unique_check() for the current
+		 * item.  This helps to avoid visiting the heap for keys, which are
+		 * anyway presented only once and can't comprise a unique violation.
+		 */
+		bool		unique_checked = false;
+
 		CHECK_FOR_INTERRUPTS();
 
 		itemid = PageGetItemIdCareful(state, state->targetblock,
@@ -1771,13 +1778,19 @@ bt_target_page_check(BtreeCheckState *state)
 
 		/*
 		 * If the index is unique verify entries uniqueness by checking the
-		 * heap tuples visibility.
+		 * heap tuples visibility.  Immediately check posting tuples and
+		 * tuples with repeated keys.  Postpone check for keys, which have the
+		 * first appearance.
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
-			P_ISLEAF(topaque) && !skey->anynullkeys)
+			P_ISLEAF(topaque) && !skey->anynullkeys &&
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+		{
 			bt_entry_unique_check(state, itup, state->targetblock, offset,
 								  &lVis_i, &lVis_tid, &lVis_offset,
 								  &lVis_block);
+			unique_checked = true;
+		}
 
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
@@ -1796,6 +1809,9 @@ bt_target_page_check(BtreeCheckState *state)
 			 * data (whole index tuple or last posting in index tuple). Key
 			 * containing null value does not violate unique constraint and
 			 * treated as different to any other key.
+			 *
+			 * If the next key is the same as the previous one, do the
+			 * bt_entry_unique_check() call if it was postponed.
 			 */
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
@@ -1805,6 +1821,12 @@ bt_target_page_check(BtreeCheckState *state)
 				lVis_block = InvalidBlockNumber;
 				lVis_offset = InvalidOffsetNumber;
 			}
+			else if (!unique_checked)
+			{
+				bt_entry_unique_check(state, itup, state->targetblock, offset,
+									  &lVis_i, &lVis_tid, &lVis_offset,
+									  &lVis_block);
+			}
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1889,6 +1911,15 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					/*
+					 * Do the bt_entry_unique_check() call if it was
+					 * postponed.
+					 */
+					if (!unique_checked)
+						bt_entry_unique_check(state, itup, state->targetblock, offset,
+											  &lVis_i, &lVis_tid, &lVis_offset,
+											  &lVis_block);
+
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
 													  rightblock_number);
-- 
2.39.3 (Apple Git-145)

v2-0004-amcheck-Report-an-error-when-the-next-page-to-a-l.patchapplication/octet-stream; name=v2-0004-amcheck-Report-an-error-when-the-next-page-to-a-l.patchDownload
From 9b36cf33f719107c899863f15ed5d0938a99b029 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:07:22 +0300
Subject: [PATCH v2 4/4] amcheck: Report an error when the next page to a leaf
 is not a leaf

This is a very unlikely condition during checking a B-tree unique constraint,
meaning that the index structure is violated badly, and we shouldn't continue
checking to avoid endless loops, etc.  So it's worth immediately throwing an
error.

Reported-by: Peter Geoghegan
Discussion: https://postgr.es/m/CAH2-Wzk%2B2116uOXdOViA27SHcr31WKPgmjsxXLBs_aTxAeThzg%40mail.gmail.com
Author: Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 977f8b6799d..e9bbc18c4a5 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1849,7 +1849,6 @@ bt_target_page_check(BtreeCheckState *state)
 		if (offset == max)
 		{
 			BTScanInsert rightkey;
-			BlockNumber rightblock_number;
 
 			/* first offset on a right index page (log only) */
 			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
@@ -1894,10 +1893,11 @@ bt_target_page_check(BtreeCheckState *state)
 			 * If index has unique constraint make sure that no more than one
 			 * found equal items is visible.
 			 */
-			rightblock_number = topaque->btpo_next;
 			if (state->checkunique && state->indexinfo->ii_Unique &&
-				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+				rightkey && P_ISLEAF(topaque) && !P_RIGHTMOST(topaque))
 			{
+				BlockNumber rightblock_number = topaque->btpo_next;
+
 				elog(DEBUG2, "check cross page unique condition");
 
 				/*
@@ -1924,9 +1924,19 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightblock_number);
 					topaque = BTPageGetOpaque(rightpage);
 
-					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
-						break;
-
+					if (P_IGNORE(topaque))
+					{
+						if (unlikely(!P_ISLEAF(topaque)))
+							ereport(ERROR,
+									(errcode(ERRCODE_INDEX_CORRUPTED),
+									 errmsg("right block of leaf block is non-leaf for index \"%s\"",
+											RelationGetRelationName(state->rel)),
+									 errdetail_internal("Block=%u page lsn=%X/%X.",
+														state->targetblock,
+														LSN_FORMAT_ARGS(state->targetlsn))));
+						else
+							break;
+					}
 					itemid = PageGetItemIdCareful(state, rightblock_number,
 												  rightpage,
 												  rightfirstoffset);
-- 
2.39.3 (Apple Git-145)

#56Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Korotkov (#55)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

regards, tom lane

#57Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#56)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Tom!

On Fri, 10 May 2024, 04:43 Tom Lane, <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

regards, tom lane

I think these patches are nice-to-have optimizations and refactorings to
make code look better. They are not necessary for the main feature. They
don't fix any bugs. But they were requested in the thread, and make sense
in my opinion.

I really don't know what's the policy of applying code improvements other
than bugfixes post feature-freeze. IMO they are safe to be appiled to v17,
but they also could be added later.

Regards,
Pavel Borisov
Supabase

Show quoted text
#58Alexander Korotkov
aekorotkov@gmail.com
In reply to: Tom Lane (#56)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?

------
Regards,
Alexander Korotkov
Supabase

#59Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Alexander Korotkov (#58)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Alexander!

On Fri, 10 May 2024 at 12:39, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?

I've re-checked patches v2. Differences from v1 are in improving
naming/pgindent's/commit messages.
In 0002 order of variables in struct BtreeLastVisibleEntry changed.
This doesn't change code behavior.

Patch v2-0003 doesn't contain credits and a discussion link. All other
patches do.

Overall, patches contain small performance optimization (0001), code
refactoring and error reporting changes. IMO they could be pushed post-FF.

Regards,
Pavel.

#60Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#59)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 10, 2024, at 5:10 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Hi, Alexander!

On Fri, 10 May 2024 at 12:39, Alexander Korotkov <aekorotkov@gmail.com> wrote:
On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?
I've re-checked patches v2. Differences from v1 are in improving naming/pgindent's/commit messages.
In 0002 order of variables in struct BtreeLastVisibleEntry changed. This doesn't change code behavior.

Patch v2-0003 doesn't contain credits and a discussion link. All other patches do.

Overall, patches contain small performance optimization (0001), code refactoring and error reporting changes. IMO they could be pushed post-FF.

v2-0001's commit message itself says, "This commit implements skipping keys". I take no position on the correctness or value of the improvement, but it seems out of scope post feature freeze. The patch seems to postpone uniqueness checking until later in the scan than what the prior version did, and that kind of change could require more analysis than we have time for at this point in the release cycle.

v2-0002 does appear to just be refactoring. I don't care for a small portion of that patch, but I doubt it violates the post feature freeze rules. In particular:

+ BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL};

v2-0003 may be an improvement in some way, but it compounds some preexisting confusion also. There is already a member of the BtreeCheckState called "target" and a memory context in that struct called "targetcontext". That context is used to allocate pages "state->target", "rightpage", "child" and "page", but not "metapage". Perhaps "targetcontext" is a poor choice of name? "notmetacontext" is a terrible name, but closer to describing the purpose of the memory context. Care to propose something sensible?

Prior to applying v2-0003, the rightpage was stored in state->target, and continued to be in state->target later when entering

/*
* * Downlink check *
*
* Additional check of child items iff this is an internal page and
* caller holds a ShareLock. This happens for every downlink (item)
* in target excluding the negative-infinity downlink (again, this is
* because it has no useful value to compare).
*/
if (!P_ISLEAF(topaque) && state->readonly)
bt_child_check(state, skey, offset);

and thereafter. Now, the rightpage of state->target is created, checked, and free'd, and then the old state->target gets processed in the downlink check and thereafter. This is either introducing a bug, or fixing one, but the commit message is totally ambiguous about whether this is a bugfix or a code cleanup or something else? I think this kind of patch should have a super clear commit message about what it thinks it is doing.

v2-0004 guards against a real threat, and is reasonable post feature freeze


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#61Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#60)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Mark!

On Fri, 10 May 2024, 21:35 Mark Dilger, <mark.dilger@enterprisedb.com>
wrote:

On May 10, 2024, at 5:10 AM, Pavel Borisov <pashkin.elfe@gmail.com>

wrote:

Hi, Alexander!

On Fri, 10 May 2024 at 12:39, Alexander Korotkov <aekorotkov@gmail.com>

wrote:

On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?
I've re-checked patches v2. Differences from v1 are in improving

naming/pgindent's/commit messages.

In 0002 order of variables in struct BtreeLastVisibleEntry changed. This

doesn't change code behavior.

Patch v2-0003 doesn't contain credits and a discussion link. All other

patches do.

Overall, patches contain small performance optimization (0001), code

refactoring and error reporting changes. IMO they could be pushed post-FF.

v2-0001's commit message itself says, "This commit implements skipping
keys". I take no position on the correctness or value of the improvement,
but it seems out of scope post feature freeze. The patch seems to postpone
uniqueness checking until later in the scan than what the prior version
did, and that kind of change could require more analysis than we have time
for at this point in the release cycle.

v2-0002 does appear to just be refactoring. I don't care for a small
portion of that patch, but I doubt it violates the post feature freeze
rules. In particular:

+ BtreeLastVisibleEntry lVis = {InvalidBlockNumber,
InvalidOffsetNumber, -1, NULL};

v2-0003 may be an improvement in some way, but it compounds some
preexisting confusion also. There is already a member of the
BtreeCheckState called "target" and a memory context in that struct called
"targetcontext". That context is used to allocate pages "state->target",
"rightpage", "child" and "page", but not "metapage". Perhaps
"targetcontext" is a poor choice of name? "notmetacontext" is a terrible
name, but closer to describing the purpose of the memory context. Care to
propose something sensible?

Prior to applying v2-0003, the rightpage was stored in state->target, and
continued to be in state->target later when entering

/*
* * Downlink check *
*
* Additional check of child items iff this is an internal page and
* caller holds a ShareLock. This happens for every downlink
(item)
* in target excluding the negative-infinity downlink (again, this
is
* because it has no useful value to compare).
*/
if (!P_ISLEAF(topaque) && state->readonly)
bt_child_check(state, skey, offset);

and thereafter. Now, the rightpage of state->target is created, checked,
and free'd, and then the old state->target gets processed in the downlink
check and thereafter. This is either introducing a bug, or fixing one, but
the commit message is totally ambiguous about whether this is a bugfix or a
code cleanup or something else? I think this kind of patch should have a
super clear commit message about what it thinks it is doing.

v2-0004 guards against a real threat, and is reasonable post feature freeze


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

IMO 0003 doesn't introduce nor fixes a bug. It loads rightpage into a local
variable, rather that to a BtreeCheckState that can have another users of
state->target afterb uniqueness check in the future, but don't have now. So
the original patch is correct, and the goal of this refactoring is to untie
rightpage fron state structure as it's used only transiently for cross-page
unuque check. It's the same style as already used bt_right_page_check_scankey()
that loads rightpage into a local variable.

For 0002 I doubt I understand your actual positiob. Could you explain what
it violates or doesn't violate?

Best regards,
Pavel.

#62Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#61)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, 10 May 2024, 22:42 Pavel Borisov, <pashkin.elfe@gmail.com> wrote:

Hi, Mark!

On Fri, 10 May 2024, 21:35 Mark Dilger, <mark.dilger@enterprisedb.com>
wrote:

On May 10, 2024, at 5:10 AM, Pavel Borisov <pashkin.elfe@gmail.com>

wrote:

Hi, Alexander!

On Fri, 10 May 2024 at 12:39, Alexander Korotkov <aekorotkov@gmail.com>

wrote:

On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes.

I'm

going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?
I've re-checked patches v2. Differences from v1 are in improving

naming/pgindent's/commit messages.

In 0002 order of variables in struct BtreeLastVisibleEntry changed.

This doesn't change code behavior.

Patch v2-0003 doesn't contain credits and a discussion link. All other

patches do.

Overall, patches contain small performance optimization (0001), code

refactoring and error reporting changes. IMO they could be pushed post-FF.

v2-0001's commit message itself says, "This commit implements skipping
keys". I take no position on the correctness or value of the improvement,
but it seems out of scope post feature freeze. The patch seems to postpone
uniqueness checking until later in the scan than what the prior version
did, and that kind of change could require more analysis than we have time
for at this point in the release cycle.

v2-0002 does appear to just be refactoring. I don't care for a small
portion of that patch, but I doubt it violates the post feature freeze
rules. In particular:

+ BtreeLastVisibleEntry lVis = {InvalidBlockNumber,
InvalidOffsetNumber, -1, NULL};

v2-0003 may be an improvement in some way, but it compounds some
preexisting confusion also. There is already a member of the
BtreeCheckState called "target" and a memory context in that struct called
"targetcontext". That context is used to allocate pages "state->target",
"rightpage", "child" and "page", but not "metapage". Perhaps
"targetcontext" is a poor choice of name? "notmetacontext" is a terrible
name, but closer to describing the purpose of the memory context. Care to
propose something sensible?

Prior to applying v2-0003, the rightpage was stored in state->target, and
continued to be in state->target later when entering

/*
* * Downlink check *
*
* Additional check of child items iff this is an internal page
and
* caller holds a ShareLock. This happens for every downlink
(item)
* in target excluding the negative-infinity downlink (again,
this is
* because it has no useful value to compare).
*/
if (!P_ISLEAF(topaque) && state->readonly)
bt_child_check(state, skey, offset);

and thereafter. Now, the rightpage of state->target is created, checked,
and free'd, and then the old state->target gets processed in the downlink
check and thereafter. This is either introducing a bug, or fixing one, but
the commit message is totally ambiguous about whether this is a bugfix or a
code cleanup or something else? I think this kind of patch should have a
super clear commit message about what it thinks it is doing.

v2-0004 guards against a real threat, and is reasonable post feature
freeze


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

IMO 0003 doesn't introduce nor fixes a bug. It loads rightpage into a
local variable, rather that to a BtreeCheckState that can have another
users of state->target afterb uniqueness check in the future, but don't
have now. So the original patch is correct, and the goal of this
refactoring is to untie rightpage fron state structure as it's used only
transiently for cross-page unuque check. It's the same style as already
used bt_right_page_check_scankey() that loads rightpage into a local
variable.

For 0002 I doubt I understand your actual positiob. Could you explain what
it violates or doesn't violate?

Please forgive many typos in the previous message, I wrote from phone.

Pavel.

Show quoted text
#63Alexander Korotkov
aekorotkov@gmail.com
In reply to: Mark Dilger (#60)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, May 10, 2024 at 8:35 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 5:10 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:
On Fri, 10 May 2024 at 12:39, Alexander Korotkov <aekorotkov@gmail.com> wrote:
On Fri, May 10, 2024 at 3:43 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexander Korotkov <aekorotkov@gmail.com> writes:

The revised patchset is attached. I applied cosmetical changes. I'm
going to push it if no objections.

Is this really suitable material to be pushing post-feature-freeze?
It doesn't look like it's fixing any new-in-v17 issues.

These are code improvements to the 5ae2087202, which answer critics in
the thread. 0001 comprises an optimization, but it's rather small and
simple. 0002 and 0003 contain refactoring. 0004 contains better
error reporting. For me this looks like pretty similar to what others
commit post-FF, isn't it?
I've re-checked patches v2. Differences from v1 are in improving naming/pgindent's/commit messages.
In 0002 order of variables in struct BtreeLastVisibleEntry changed. This doesn't change code behavior.

Patch v2-0003 doesn't contain credits and a discussion link. All other patches do.

Overall, patches contain small performance optimization (0001), code refactoring and error reporting changes. IMO they could be pushed post-FF.

v2-0001's commit message itself says, "This commit implements skipping keys". I take no position on the correctness or value of the improvement, but it seems out of scope post feature freeze. The patch seems to postpone uniqueness checking until later in the scan than what the prior version did, and that kind of change could require more analysis than we have time for at this point in the release cycle.

Formally this could be classified as algorithmic change and probably
should be postponed to the next release. But that's quite local
optimization, which just postpones a function call within the same
iteration of loop. And the effect is huge. Probably we could allow
this post-FF in the sake of quality release, given it's very local
change with a huge effect.

v2-0002 does appear to just be refactoring. I don't care for a small portion of that patch, but I doubt it violates the post feature freeze rules. In particular:

+ BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL};

I don't understand what is the problem with this line and post feature
freeze rules. Please, explain it more.

v2-0003 may be an improvement in some way, but it compounds some preexisting confusion also. There is already a member of the BtreeCheckState called "target" and a memory context in that struct called "targetcontext". That context is used to allocate pages "state->target", "rightpage", "child" and "page", but not "metapage". Perhaps "targetcontext" is a poor choice of name? "notmetacontext" is a terrible name, but closer to describing the purpose of the memory context. Care to propose something sensible?

Prior to applying v2-0003, the rightpage was stored in state->target, and continued to be in state->target later when entering

/*
* * Downlink check *
*
* Additional check of child items iff this is an internal page and
* caller holds a ShareLock. This happens for every downlink (item)
* in target excluding the negative-infinity downlink (again, this is
* because it has no useful value to compare).
*/
if (!P_ISLEAF(topaque) && state->readonly)
bt_child_check(state, skey, offset);

and thereafter. Now, the rightpage of state->target is created, checked, and free'd, and then the old state->target gets processed in the downlink check and thereafter. This is either introducing a bug, or fixing one, but the commit message is totally ambiguous about whether this is a bugfix or a code cleanup or something else? I think this kind of patch should have a super clear commit message about what it thinks it is doing.

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

0002 and 0003 don't address any bugs, but It would be very nice to
accept them, because it would simplify future backpatching in this
area.

v2-0004 guards against a real threat, and is reasonable post feature freeze

Ok.

------
Regards,
Alexander Korotkov
Supabase

#64Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#61)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 10, 2024, at 11:42 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

IMO 0003 doesn't introduce nor fixes a bug. It loads rightpage into a local variable, rather that to a BtreeCheckState that can have another users of state->target afterb uniqueness check in the future, but don't have now. So the original patch is correct, and the goal of this refactoring is to untie rightpage fron state structure as it's used only transiently for cross-page unuque check. It's the same style as already used bt_right_page_check_scankey() that loads rightpage into a local variable.

Well, you can put an Assert(false) dead in the middle of the code we're discussing and all the regression tests still pass, so I'd argue the change is getting zero test coverage.

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?

Having a regression test that actually touches this code would go a fair way towards helping the analysis.

For 0002 I doubt I understand your actual positiob. Could you explain what it violates or doesn't violate?

v2-0002 is does not violate the post feature freeze restriction on new features so far as I can tell, but I just don't care for the variable initialization because it doesn't name the fields. If anybody refactored the struct they might not notice that the need to reorder this initialization, and depending on various factors including compiler flags they might not get an error.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#65Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Alexander Korotkov (#63)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 10, 2024, at 12:05 PM, Alexander Korotkov <aekorotkov@gmail.com> wrote:

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before bt_check_level_from_leftmost() overrides state->target in the next iteration, bt_check_level_from_leftmost() conditionally fetches an item from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses "rightpage" rather than "state->target" in the same iteration where bt_check_level_from_leftmost() conditionally fetches an item from state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target in an inappropriate way, causing the wrong thing to happen at what is now line 963. The patch fixes the bug, because state->target no longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong to wrong.

If you believe (1) is true, then I'm complaining that you are relying far to much on action at a distance, and that you are not documenting it. Even with documentation of this interrelationship, I'd be unhappy with how brittle the code is. I cannot easily discern that the two don't ever happen in the same iteration, and I'm not at all convinced one way or the other. I tried to set up some Asserts about that, but none of the test cases actually reach the new code, so adding Asserts doesn't help to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't mention the fact that this is a bug fix. Bug fixes should be clearly documented as such, otherwise future work might assume the commit can be reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

For reference, I said something similar earlier today in another email to this thread:

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#66Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mark Dilger (#65)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Mark Dilger <mark.dilger@enterprisedb.com> writes:

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

Indeed. If we have no regression tests that reach this code, it's
folly to touch it at all, but most especially so post-feature-freeze.

I think the *first* order of business ought to be to create some
test cases that reach this area. Perhaps they'll be too expensive
to incorporate in our regular regression tests, but we could still
use them to investigate Mark's concerns.

regards, tom lane

#67Alexander Korotkov
aekorotkov@gmail.com
In reply to: Mark Dilger (#65)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <aekorotkov@gmail.com> wrote:
The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before bt_check_level_from_leftmost() overrides state->target in the next iteration, bt_check_level_from_leftmost() conditionally fetches an item from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses "rightpage" rather than "state->target" in the same iteration where bt_check_level_from_leftmost() conditionally fetches an item from state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target in an inappropriate way, causing the wrong thing to happen at what is now line 963. The patch fixes the bug, because state->target no longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong to wrong.

If you believe (1) is true, then I'm complaining that you are relying far to much on action at a distance, and that you are not documenting it. Even with documentation of this interrelationship, I'd be unhappy with how brittle the code is. I cannot easily discern that the two don't ever happen in the same iteration, and I'm not at all convinced one way or the other. I tried to set up some Asserts about that, but none of the test cases actually reach the new code, so adding Asserts doesn't help to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't mention the fact that this is a bug fix. Bug fixes should be clearly documented as such, otherwise future work might assume the commit can be reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

For reference, I said something similar earlier today in another email to this thread:

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

------
Regards,
Alexander Korotkov

#68Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#67)
4 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <aekorotkov@gmail.com> wrote:
The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before bt_check_level_from_leftmost() overrides state->target in the next iteration, bt_check_level_from_leftmost() conditionally fetches an item from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses "rightpage" rather than "state->target" in the same iteration where bt_check_level_from_leftmost() conditionally fetches an item from state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target in an inappropriate way, causing the wrong thing to happen at what is now line 963. The patch fixes the bug, because state->target no longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong to wrong.

If you believe (1) is true, then I'm complaining that you are relying far to much on action at a distance, and that you are not documenting it. Even with documentation of this interrelationship, I'd be unhappy with how brittle the code is. I cannot easily discern that the two don't ever happen in the same iteration, and I'm not at all convinced one way or the other. I tried to set up some Asserts about that, but none of the test cases actually reach the new code, so adding Asserts doesn't help to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't mention the fact that this is a bug fix. Bug fixes should be clearly documented as such, otherwise future work might assume the commit can be reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

For reference, I said something similar earlier today in another email to this thread:

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

So, the picture for the patches is the following now.
0001 – optimization, but rather simple and giving huge effect
0002 – refactoring
0003 – fix for the bug
0004 – better error reporting

------
Regards,
Alexander Korotkov

Attachments:

v3-0002-amcheck-Refactoring-the-storage-of-the-last-visib.patchapplication/octet-stream; name=v3-0002-amcheck-Refactoring-the-storage-of-the-last-visib.patchDownload
From 7014fa433ba5c3c6ea645b13d433a11f4b5a0b15 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:08:07 +0300
Subject: [PATCH v3 2/4] amcheck: Refactoring the storage of the last visible
 entry

This commit introduces a new data structure BtreeLastVisibleEntry comprising
information about the last visible heap entry with the current value of key.
Usage of this data structure allows us to avoid passing all this information
as individual function arguments.

Reported-by: Alexander Korotkov
Discussion: https://www.postgresql.org/message-id/CAPpHfdsVbB9ToriaB1UHuOKwjKxiZmTFQcEF%3DjuzzC_nby31uA%40mail.gmail.com
Author: Pavel Borisov, Alexander Korotkov
---
 contrib/amcheck/verify_nbtree.c  | 125 +++++++++++++++----------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 61 insertions(+), 65 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index c7be785f88b..b433cb33254 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -145,6 +145,19 @@ typedef struct BtreeLevel
 	bool		istruerootlevel;
 } BtreeLevel;
 
+/*
+ * Information about the last visible entry with current B-tree key.  Used
+ * for validation of the unique constraint.
+ */
+typedef struct BtreeLastVisibleEntry
+{
+	BlockNumber blkno;			/* Index block */
+	OffsetNumber offset;		/* Offset on index block */
+	int			postingIndex;	/* Number in the posting list (-1 for
+								 * non-deduplicated tuples) */
+	ItemPointer tid;			/* Heap tid */
+} BtreeLastVisibleEntry;
+
 PG_FUNCTION_INFO_V1(bt_index_check);
 PG_FUNCTION_INFO_V1(bt_index_parent_check);
 
@@ -165,17 +178,13 @@ static void bt_recheck_sibling_links(BtreeCheckState *state,
 									 BlockNumber btpo_prev_from_target,
 									 BlockNumber leftcurrent);
 static bool heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid);
-static void bt_report_duplicate(BtreeCheckState *state, ItemPointer tid,
-								BlockNumber block, OffsetNumber offset,
-								int posting, ItemPointer nexttid,
+static void bt_report_duplicate(BtreeCheckState *state, BtreeLastVisibleEntry *lVis,
+								ItemPointer nexttid,
 								BlockNumber nblock, OffsetNumber noffset,
 								int nposting);
 static void bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 								  BlockNumber targetblock,
-								  OffsetNumber offset, int *lVis_i,
-								  ItemPointer *lVis_tid,
-								  OffsetNumber *lVis_offset,
-								  BlockNumber *lVis_block);
+								  OffsetNumber offset, BtreeLastVisibleEntry *lVis);
 static void bt_target_page_check(BtreeCheckState *state);
 static BTScanInsert bt_right_page_check_scankey(BtreeCheckState *state,
 												OffsetNumber *rightfirstoffset);
@@ -997,8 +1006,7 @@ heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid)
  */
 static void
 bt_report_duplicate(BtreeCheckState *state,
-					ItemPointer tid, BlockNumber block, OffsetNumber offset,
-					int posting,
+					BtreeLastVisibleEntry *lVis,
 					ItemPointer nexttid, BlockNumber nblock, OffsetNumber noffset,
 					int nposting)
 {
@@ -1010,18 +1018,18 @@ bt_report_duplicate(BtreeCheckState *state,
 			   *pnposting = "";
 
 	htid = psprintf("tid=(%u,%u)",
-					ItemPointerGetBlockNumberNoCheck(tid),
-					ItemPointerGetOffsetNumberNoCheck(tid));
+					ItemPointerGetBlockNumberNoCheck(lVis->tid),
+					ItemPointerGetOffsetNumberNoCheck(lVis->tid));
 	nhtid = psprintf("tid=(%u,%u)",
 					 ItemPointerGetBlockNumberNoCheck(nexttid),
 					 ItemPointerGetOffsetNumberNoCheck(nexttid));
-	itid = psprintf("tid=(%u,%u)", block, offset);
+	itid = psprintf("tid=(%u,%u)", lVis->blkno, lVis->offset);
 
-	if (nblock != block || noffset != offset)
+	if (nblock != lVis->blkno || noffset != lVis->offset)
 		nitid = psprintf(" tid=(%u,%u)", nblock, noffset);
 
-	if (posting >= 0)
-		pposting = psprintf(" posting %u", posting);
+	if (lVis->postingIndex >= 0)
+		pposting = psprintf(" posting %u", lVis->postingIndex);
 
 	if (nposting >= 0)
 		pnposting = psprintf(" posting %u", nposting);
@@ -1038,9 +1046,7 @@ bt_report_duplicate(BtreeCheckState *state,
 /* Check if current nbtree leaf entry complies with UNIQUE constraint */
 static void
 bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
-					  BlockNumber targetblock, OffsetNumber offset, int *lVis_i,
-					  ItemPointer *lVis_tid, OffsetNumber *lVis_offset,
-					  BlockNumber *lVis_block)
+					  BlockNumber targetblock, OffsetNumber offset, BtreeLastVisibleEntry *lVis)
 {
 	ItemPointer tid;
 	bool		has_visible_entry = false;
@@ -1049,7 +1055,7 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 
 	/*
 	 * Current tuple has posting list. Report duplicate if TID of any posting
-	 * list entry is visible and lVis_tid is valid.
+	 * list entry is visible and lVis->tid is valid.
 	 */
 	if (BTreeTupleIsPosting(itup))
 	{
@@ -1059,11 +1065,10 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 			if (heap_entry_is_visible(state, tid))
 			{
 				has_visible_entry = true;
-				if (ItemPointerIsValid(*lVis_tid))
+				if (ItemPointerIsValid(lVis->tid))
 				{
 					bt_report_duplicate(state,
-										*lVis_tid, *lVis_block,
-										*lVis_offset, *lVis_i,
+										lVis,
 										tid, targetblock,
 										offset, i);
 				}
@@ -1073,13 +1078,13 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 				 * between the posting list entries of the first tuple on the
 				 * page after cross-page check.
 				 */
-				if (*lVis_block != targetblock && ItemPointerIsValid(*lVis_tid))
+				if (lVis->blkno != targetblock && ItemPointerIsValid(lVis->tid))
 					return;
 
-				*lVis_i = i;
-				*lVis_tid = tid;
-				*lVis_offset = offset;
-				*lVis_block = targetblock;
+				lVis->blkno = targetblock;
+				lVis->offset = offset;
+				lVis->postingIndex = i;
+				lVis->tid = tid;
 			}
 		}
 	}
@@ -1087,7 +1092,7 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 	/*
 	 * Current tuple has no posting list. If TID is visible save info about it
 	 * for the next comparisons in the loop in bt_target_page_check(). Report
-	 * duplicate if lVis_tid is already valid.
+	 * duplicate if lVis->tid is already valid.
 	 */
 	else
 	{
@@ -1095,37 +1100,38 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup,
 		if (heap_entry_is_visible(state, tid))
 		{
 			has_visible_entry = true;
-			if (ItemPointerIsValid(*lVis_tid))
+			if (ItemPointerIsValid(lVis->tid))
 			{
 				bt_report_duplicate(state,
-									*lVis_tid, *lVis_block,
-									*lVis_offset, *lVis_i,
+									lVis,
 									tid, targetblock,
 									offset, -1);
 			}
-			*lVis_i = -1;
-			*lVis_tid = tid;
-			*lVis_offset = offset;
-			*lVis_block = targetblock;
+
+			lVis->blkno = targetblock;
+			lVis->offset = offset;
+			lVis->tid = tid;
+			lVis->postingIndex = -1;
 		}
 	}
 
-	if (!has_visible_entry && *lVis_block != InvalidBlockNumber &&
-		*lVis_block != targetblock)
+	if (!has_visible_entry &&
+		lVis->blkno != InvalidBlockNumber &&
+		lVis->blkno != targetblock)
 	{
 		char	   *posting = "";
 
-		if (*lVis_i >= 0)
-			posting = psprintf(" posting %u", *lVis_i);
+		if (lVis->postingIndex >= 0)
+			posting = psprintf(" posting %u", lVis->postingIndex);
 		ereport(DEBUG1,
 				(errcode(ERRCODE_NO_DATA),
 				 errmsg("index uniqueness can not be checked for index tid=(%u,%u) in index \"%s\"",
 						targetblock, offset,
 						RelationGetRelationName(state->rel)),
 				 errdetail("It doesn't have visible heap tids and key is equal to the tid=(%u,%u)%s (points to heap tid=(%u,%u)).",
-						   *lVis_block, *lVis_offset, posting,
-						   ItemPointerGetBlockNumberNoCheck(*lVis_tid),
-						   ItemPointerGetOffsetNumberNoCheck(*lVis_tid)),
+						   lVis->blkno, lVis->offset, posting,
+						   ItemPointerGetBlockNumberNoCheck(lVis->tid),
+						   ItemPointerGetOffsetNumberNoCheck(lVis->tid)),
 				 errhint("VACUUM the table and repeat the check.")));
 	}
 }
@@ -1372,12 +1378,8 @@ bt_target_page_check(BtreeCheckState *state)
 	OffsetNumber max;
 	BTPageOpaque topaque;
 
-	/* last visible entry info for checking indexes with unique constraint */
-	int			lVis_i = -1;	/* the position of last visible item for
-								 * posting tuple. for non-posting tuple (-1) */
-	ItemPointer lVis_tid = NULL;
-	BlockNumber lVis_block = InvalidBlockNumber;
-	OffsetNumber lVis_offset = InvalidOffsetNumber;
+	/* Last visible entry info for checking indexes with unique constraint */
+	BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL};
 
 	topaque = BTPageGetOpaque(state->target);
 	max = PageGetMaxOffsetNumber(state->target);
@@ -1784,11 +1786,9 @@ bt_target_page_check(BtreeCheckState *state)
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && !skey->anynullkeys &&
-			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis.tid)))
 		{
-			bt_entry_unique_check(state, itup, state->targetblock, offset,
-								  &lVis_i, &lVis_tid, &lVis_offset,
-								  &lVis_block);
+			bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			unique_checked = true;
 		}
 
@@ -1816,17 +1816,16 @@ bt_target_page_check(BtreeCheckState *state)
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
 			{
-				lVis_i = -1;
-				lVis_tid = NULL;
-				lVis_block = InvalidBlockNumber;
-				lVis_offset = InvalidOffsetNumber;
+				lVis.blkno = InvalidBlockNumber;
+				lVis.offset = InvalidOffsetNumber;
+				lVis.postingIndex = -1;
+				lVis.tid = NULL;
 			}
 			else if (!unique_checked)
 			{
-				bt_entry_unique_check(state, itup, state->targetblock, offset,
-									  &lVis_i, &lVis_tid, &lVis_offset,
-									  &lVis_block);
+				bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 			}
+
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1916,9 +1915,7 @@ bt_target_page_check(BtreeCheckState *state)
 					 * postponed.
 					 */
 					if (!unique_checked)
-						bt_entry_unique_check(state, itup, state->targetblock, offset,
-											  &lVis_i, &lVis_tid, &lVis_offset,
-											  &lVis_block);
+						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
@@ -1933,9 +1930,7 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightfirstoffset);
 					itup = (IndexTuple) PageGetItem(state->target, itemid);
 
-					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset,
-										  &lVis_i, &lVis_tid, &lVis_offset,
-										  &lVis_block);
+					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
 				}
 			}
 		}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 34ec87a85eb..8d24418fe3c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -315,6 +315,7 @@ BrinStatsData
 BrinTuple
 BrinValues
 BtreeCheckState
+BtreeLastVisibleEntry
 BtreeLevel
 Bucket
 BufFile
-- 
2.39.3 (Apple Git-145)

v3-0004-amcheck-Report-an-error-when-the-next-page-to-a-l.patchapplication/octet-stream; name=v3-0004-amcheck-Report-an-error-when-the-next-page-to-a-l.patchDownload
From b4645780074cdb0a3abdb7c84435d6abad4e3bec Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:07:22 +0300
Subject: [PATCH v3 4/4] amcheck: Report an error when the next page to a leaf
 is not a leaf

This is a very unlikely condition during checking a B-tree unique constraint,
meaning that the index structure is violated badly, and we shouldn't continue
checking to avoid endless loops, etc.  So it's worth immediately throwing an
error.

Reported-by: Peter Geoghegan
Discussion: https://postgr.es/m/CAH2-Wzk%2B2116uOXdOViA27SHcr31WKPgmjsxXLBs_aTxAeThzg%40mail.gmail.com
Author: Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 977f8b6799d..e9bbc18c4a5 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1849,7 +1849,6 @@ bt_target_page_check(BtreeCheckState *state)
 		if (offset == max)
 		{
 			BTScanInsert rightkey;
-			BlockNumber rightblock_number;
 
 			/* first offset on a right index page (log only) */
 			OffsetNumber rightfirstoffset = InvalidOffsetNumber;
@@ -1894,10 +1893,11 @@ bt_target_page_check(BtreeCheckState *state)
 			 * If index has unique constraint make sure that no more than one
 			 * found equal items is visible.
 			 */
-			rightblock_number = topaque->btpo_next;
 			if (state->checkunique && state->indexinfo->ii_Unique &&
-				rightkey && P_ISLEAF(topaque) && rightblock_number != P_NONE)
+				rightkey && P_ISLEAF(topaque) && !P_RIGHTMOST(topaque))
 			{
+				BlockNumber rightblock_number = topaque->btpo_next;
+
 				elog(DEBUG2, "check cross page unique condition");
 
 				/*
@@ -1924,9 +1924,19 @@ bt_target_page_check(BtreeCheckState *state)
 												  rightblock_number);
 					topaque = BTPageGetOpaque(rightpage);
 
-					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
-						break;
-
+					if (P_IGNORE(topaque))
+					{
+						if (unlikely(!P_ISLEAF(topaque)))
+							ereport(ERROR,
+									(errcode(ERRCODE_INDEX_CORRUPTED),
+									 errmsg("right block of leaf block is non-leaf for index \"%s\"",
+											RelationGetRelationName(state->rel)),
+									 errdetail_internal("Block=%u page lsn=%X/%X.",
+														state->targetblock,
+														LSN_FORMAT_ARGS(state->targetlsn))));
+						else
+							break;
+					}
 					itemid = PageGetItemIdCareful(state, rightblock_number,
 												  rightpage,
 												  rightfirstoffset);
-- 
2.39.3 (Apple Git-145)

v3-0003-amcheck-Don-t-load-the-right-sibling-page-into-Bt.patchapplication/octet-stream; name=v3-0003-amcheck-Don-t-load-the-right-sibling-page-into-Bt.patchDownload
From e9041f10384250472a54bd75a1cf28f89ec15e32 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 13 May 2024 04:18:56 +0300
Subject: [PATCH v3 3/4] amcheck: Don't load the right sibling page into
 BtreeCheckState

5ae2087202 implemented a cross-page unique constraint check by loading
the right sibling to the BtreeCheckState.target variable.  This is wrong,
because bt_target_page_check() shouldn't change the target page.  Also,
BtreeCheckState.target shouldn't be changed alone without
BtreeCheckState.targetblock.

However, the above didn't cause any visible bugs for the following reasons.
1. We do a cross-page unique constraint check only for leaf index pages.
2. The only way target page get accessed after a cross-page unique constraint
   check is loading of the lowkey.
3. The only place lowkey is used is bt_child_highkey_check(), and that applies
   only to non-leafs.

The reasons above don't diminish the fact that changing BtreeCheckState.target
for a cross-page unique constraint check is wrong.  This commit changes this
check to temporarily store the right sibling to the local variable.

Reported-by: Peter Geoghegan
Discussion: https://postgr.es/m/CAH2-Wzk%2B2116uOXdOViA27SHcr31WKPgmjsxXLBs_aTxAeThzg%40mail.gmail.com
Author: Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b433cb33254..977f8b6799d 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1910,6 +1910,8 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					Page		rightpage;
+
 					/*
 					 * Do the bt_entry_unique_check() call if it was
 					 * postponed.
@@ -1918,19 +1920,21 @@ bt_target_page_check(BtreeCheckState *state)
 						bt_entry_unique_check(state, itup, state->targetblock, offset, &lVis);
 
 					elog(DEBUG2, "cross page equal keys");
-					state->target = palloc_btree_page(state,
-													  rightblock_number);
-					topaque = BTPageGetOpaque(state->target);
+					rightpage = palloc_btree_page(state,
+												  rightblock_number);
+					topaque = BTPageGetOpaque(rightpage);
 
 					if (P_IGNORE(topaque) || !P_ISLEAF(topaque))
 						break;
 
 					itemid = PageGetItemIdCareful(state, rightblock_number,
-												  state->target,
+												  rightpage,
 												  rightfirstoffset);
-					itup = (IndexTuple) PageGetItem(state->target, itemid);
+					itup = (IndexTuple) PageGetItem(rightpage, itemid);
 
 					bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis);
+
+					pfree(rightpage);
 				}
 			}
 		}
-- 
2.39.3 (Apple Git-145)

v3-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchapplication/octet-stream; name=v3-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchDownload
From f801783ade88e524b73acfa19f1b06e5b54608df Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:07:53 +0300
Subject: [PATCH v3 1/4] amcheck: Optimize speed of checking for unique
 constraint violation

Currently, when amcheck validates a unique constraint, it visits the heap for
each index tuple.  This commit implements skipping keys, which have only one
non-dedeuplicated index tuple (quite common case for unique indexes). That
gives substantial economy on index checking time.

Reported-by: Noah Misch
Discussion: https://postgr.es/m/20240325020323.fd.nmisch%40google.com
Author: Alexander Korotkov, Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 35 +++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 70f65b645a6..c7be785f88b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1429,6 +1429,13 @@ bt_target_page_check(BtreeCheckState *state)
 		bool		lowersizelimit;
 		ItemPointer scantid;
 
+		/*
+		 * True if we already called bt_entry_unique_check() for the current
+		 * item.  This helps to avoid visiting the heap for keys, which are
+		 * anyway presented only once and can't comprise a unique violation.
+		 */
+		bool		unique_checked = false;
+
 		CHECK_FOR_INTERRUPTS();
 
 		itemid = PageGetItemIdCareful(state, state->targetblock,
@@ -1771,13 +1778,19 @@ bt_target_page_check(BtreeCheckState *state)
 
 		/*
 		 * If the index is unique verify entries uniqueness by checking the
-		 * heap tuples visibility.
+		 * heap tuples visibility.  Immediately check posting tuples and
+		 * tuples with repeated keys.  Postpone check for keys, which have the
+		 * first appearance.
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
-			P_ISLEAF(topaque) && !skey->anynullkeys)
+			P_ISLEAF(topaque) && !skey->anynullkeys &&
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis_tid)))
+		{
 			bt_entry_unique_check(state, itup, state->targetblock, offset,
 								  &lVis_i, &lVis_tid, &lVis_offset,
 								  &lVis_block);
+			unique_checked = true;
+		}
 
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
@@ -1796,6 +1809,9 @@ bt_target_page_check(BtreeCheckState *state)
 			 * data (whole index tuple or last posting in index tuple). Key
 			 * containing null value does not violate unique constraint and
 			 * treated as different to any other key.
+			 *
+			 * If the next key is the same as the previous one, do the
+			 * bt_entry_unique_check() call if it was postponed.
 			 */
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
@@ -1805,6 +1821,12 @@ bt_target_page_check(BtreeCheckState *state)
 				lVis_block = InvalidBlockNumber;
 				lVis_offset = InvalidOffsetNumber;
 			}
+			else if (!unique_checked)
+			{
+				bt_entry_unique_check(state, itup, state->targetblock, offset,
+									  &lVis_i, &lVis_tid, &lVis_offset,
+									  &lVis_block);
+			}
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1889,6 +1911,15 @@ bt_target_page_check(BtreeCheckState *state)
 				/* The first key on the next page is the same */
 				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
 				{
+					/*
+					 * Do the bt_entry_unique_check() call if it was
+					 * postponed.
+					 */
+					if (!unique_checked)
+						bt_entry_unique_check(state, itup, state->targetblock, offset,
+											  &lVis_i, &lVis_tid, &lVis_offset,
+											  &lVis_block);
+
 					elog(DEBUG2, "cross page equal keys");
 					state->target = palloc_btree_page(state,
 													  rightblock_number);
-- 
2.39.3 (Apple Git-145)

#69Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Alexander Korotkov (#68)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Alexander!

On Mon, 13 May 2024 at 05:42, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <

aekorotkov@gmail.com> wrote:

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before

bt_check_level_from_leftmost() overrides state->target in the next
iteration, bt_check_level_from_leftmost() conditionally fetches an item
from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses

"rightpage" rather than "state->target" in the same iteration where
bt_check_level_from_leftmost() conditionally fetches an item from
state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target

in an inappropriate way, causing the wrong thing to happen at what is now
line 963. The patch fixes the bug, because state->target no longer gets
overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in

the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong

to wrong.

If you believe (1) is true, then I'm complaining that you are relying

far to much on action at a distance, and that you are not documenting it.
Even with documentation of this interrelationship, I'd be unhappy with how
brittle the code is. I cannot easily discern that the two don't ever
happen in the same iteration, and I'm not at all convinced one way or the
other. I tried to set up some Asserts about that, but none of the test
cases actually reach the new code, so adding Asserts doesn't help to
investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't

mention the fact that this is a bug fix. Bug fixes should be clearly
documented as such, otherwise future work might assume the commit can be
reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or

have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with

more regression test coverage.

For reference, I said something similar earlier today in another email

to this thread:

This patch introduces a change that stores a new page into variable

"rightpage" rather than overwriting "state->target", which the old
implementation most certainly did. That means that after returning from
bt_target_page_check() into the calling function
bt_check_level_from_leftmost() the value in state->target is not what it
would have been prior to this patch. Now, that'd be irrelevant if nobody
goes on to consult that value, but just 44 lines further down in
bt_check_level_from_leftmost() state->target is clearly used. So the
behavior at that point is changing between the old and new versions of the
code, and I think I'm within reason to ask if it was wrong before the
patch, wrong after the patch, or something else? Is this a bug being
introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

I agree with your analysis regarding state->target:
- when the unique check is on, state->target was reassigned only for the
leaf pages (under P_ISLEAF(topaque) in bt_target_page_check).
- in this level (leaf) in bt_check_level_from_leftmost() this value of
state->target was used to get state->lowkey. Then it was reset (in the next
iteration of do loop in in bt_check_level_from_leftmost()
- state->lowkey lives until the end of pages level (leaf) iteration cycle.
Then, low-key is reset (state->lowkey = NULL in the end of
bt_check_level_from_leftmost())
- state->lowkey is used only in bt_child_check/bt_child_highkey_check. Both
are called only from non-leaf pages iteration cycles (under
P_ISLEAF(topaque))
- Also there is a check (rightblock_number != P_NONE) in before getting
rightpage into state->target in bt_target_page_check() that ensures us that
rightpage indeed exists and getting this (unused) lowkey in
bt_check_level_from_leftmost will not invoke any page reading errors.

I'm pretty sure that there was no bug in this, not just the bug was hidden.

Indeed re-assigning state->target in leaf page iteration for cross-page
unique check was not beautiful, and Peter pointed out this. In my opinion
the patch 0003 is a pure code refactoring.

As for the cross-page check regression/TAP testing, this test had problems
since the btree page layout is not fixed (especially it's different on
32-bit arch). I had a variant for testing cross-page check when the test
was yet regression one upthread for both 32/64 bit architectures. I
remember it was decided not to include it due to complications and low
impact for testing the corner case of very rare cross-page duplicates.
(There were also suggestions to drop cross-page duplicates check at all,
which I didn't agree 2 years ago, but still it can make sense)

Separately, I propose to avoid getting state->lowkey for leaf pages at all
as it's unused. PFA is a simple patch for this. (I don't add it to the
current patch set as I believe it has nothing to do with UNIQUE constraint
check, rather it improves the previous btree amcheck code)

Best regards,
Pavel Borisov,
Supabase

Attachments:

XXXX-amcheck-Get-lowkey-only-for-internal-pages-of-btree-.patchapplication/octet-stream; name=XXXX-amcheck-Get-lowkey-only-for-internal-pages-of-btree-.patchDownload
From 9e3903fd2497c967aa001010b20167363963017a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 13 May 2024 15:19:16 +0400
Subject: [PATCH] amcheck: Get lowkey only for internal pages of btree index

state->lowkey is used only for checking child pages in btree index inside
bt_child_check()/bt_child_highkey_check() All calls of these functions
possible only from internal pages level. So there is no reason of getting
page lowkey for leaf pages just to invalidate it next without actual usage.
---
 contrib/amcheck/verify_nbtree.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 70f65b645a..8e18a19deb 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -938,13 +938,13 @@ nextpage:
 		 * falls to the boundary of pages on the target level.  See
 		 * bt_child_highkey_check() for details.  So, typically we won't end
 		 * up doing anything with low key, but it's simpler for general case
-		 * high key verification to always have it available.
+		 * high key verification to have it available for all non-leaf pages.
 		 *
 		 * The correctness of managing low key in the case of concurrent
 		 * splits wasn't investigated yet.  Thankfully we only need low key
 		 * for readonly verification and concurrent splits won't happen.
 		 */
-		if (state->readonly && !P_RIGHTMOST(opaque))
+		if (state->readonly && !P_RIGHTMOST(opaque) && !P_ISLEAF(opaque))
 		{
 			IndexTuple	itup;
 			ItemId		itemid;
-- 
2.34.1

#70Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#69)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, 13 May 2024 at 15:55, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Hi, Alexander!

On Mon, 13 May 2024 at 05:42, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <

aekorotkov@gmail.com> wrote:

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before

bt_check_level_from_leftmost() overrides state->target in the next
iteration, bt_check_level_from_leftmost() conditionally fetches an item
from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses

"rightpage" rather than "state->target" in the same iteration where
bt_check_level_from_leftmost() conditionally fetches an item from
state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target

in an inappropriate way, causing the wrong thing to happen at what is now
line 963. The patch fixes the bug, because state->target no longer gets
overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in

the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong

to wrong.

If you believe (1) is true, then I'm complaining that you are relying

far to much on action at a distance, and that you are not documenting it.
Even with documentation of this interrelationship, I'd be unhappy with how
brittle the code is. I cannot easily discern that the two don't ever
happen in the same iteration, and I'm not at all convinced one way or the
other. I tried to set up some Asserts about that, but none of the test
cases actually reach the new code, so adding Asserts doesn't help to
investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't

mention the fact that this is a bug fix. Bug fixes should be clearly
documented as such, otherwise future work might assume the commit can be
reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or

have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with

more regression test coverage.

For reference, I said something similar earlier today in another

email to this thread:

This patch introduces a change that stores a new page into variable

"rightpage" rather than overwriting "state->target", which the old
implementation most certainly did. That means that after returning from
bt_target_page_check() into the calling function
bt_check_level_from_leftmost() the value in state->target is not what it
would have been prior to this patch. Now, that'd be irrelevant if nobody
goes on to consult that value, but just 44 lines further down in
bt_check_level_from_leftmost() state->target is clearly used. So the
behavior at that point is changing between the old and new versions of the
code, and I think I'm within reason to ask if it was wrong before the
patch, wrong after the patch, or something else? Is this a bug being
introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

I agree with your analysis regarding state->target:
- when the unique check is on, state->target was reassigned only for the
leaf pages (under P_ISLEAF(topaque) in bt_target_page_check).
- in this level (leaf) in bt_check_level_from_leftmost() this value of
state->target was used to get state->lowkey. Then it was reset (in the next
iteration of do loop in in bt_check_level_from_leftmost()
- state->lowkey lives until the end of pages level (leaf) iteration cycle.
Then, low-key is reset (state->lowkey = NULL in the end of
bt_check_level_from_leftmost())
- state->lowkey is used only in bt_child_check/bt_child_highkey_check.
Both are called only from non-leaf pages iteration cycles (under
P_ISLEAF(topaque))
- Also there is a check (rightblock_number != P_NONE) in before getting
rightpage into state->target in bt_target_page_check() that ensures us that
rightpage indeed exists and getting this (unused) lowkey in
bt_check_level_from_leftmost will not invoke any page reading errors.

I'm pretty sure that there was no bug in this, not just the bug was hidden.

Indeed re-assigning state->target in leaf page iteration for cross-page
unique check was not beautiful, and Peter pointed out this. In my opinion
the patch 0003 is a pure code refactoring.

As for the cross-page check regression/TAP testing, this test had problems
since the btree page layout is not fixed (especially it's different on
32-bit arch). I had a variant for testing cross-page check when the test
was yet regression one upthread for both 32/64 bit architectures. I
remember it was decided not to include it due to complications and low
impact for testing the corner case of very rare cross-page duplicates.
(There were also suggestions to drop cross-page duplicates check at all,
which I didn't agree 2 years ago, but still it can make sense)

Separately, I propose to avoid getting state->lowkey for leaf pages at all
as it's unused. PFA is a simple patch for this. (I don't add it to the
current patch set as I believe it has nothing to do with UNIQUE constraint
check, rather it improves the previous btree amcheck code)

A correction of a typo in previous message:
non-leaf pages iteration cycles (under !P_ISLEAF(topaque)) -> non-leaf
pages iteration cycles (under !P_ISLEAF(topaque))

#71Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#70)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

A correction of a typo in previous message:
non-leaf pages iteration cycles (under P_ISLEAF(topaque)) -> non-leaf pages
iteration cycles (under !P_ISLEAF(topaque))

On Mon, 13 May 2024 at 16:19, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Show quoted text

On Mon, 13 May 2024 at 15:55, Pavel Borisov <pashkin.elfe@gmail.com>
wrote:

Hi, Alexander!

On Mon, 13 May 2024 at 05:42, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <

aekorotkov@gmail.com> wrote:

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in

the

next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before

bt_check_level_from_leftmost() overrides state->target in the next
iteration, bt_check_level_from_leftmost() conditionally fetches an item
from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses

"rightpage" rather than "state->target" in the same iteration where
bt_check_level_from_leftmost() conditionally fetches an item from
state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed

state->target in an inappropriate way, causing the wrong thing to happen at
what is now line 963. The patch fixes the bug, because state->target no
longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in

the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from

wrong to wrong.

If you believe (1) is true, then I'm complaining that you are

relying far to much on action at a distance, and that you are not
documenting it. Even with documentation of this interrelationship, I'd be
unhappy with how brittle the code is. I cannot easily discern that the two
don't ever happen in the same iteration, and I'm not at all convinced one
way or the other. I tried to set up some Asserts about that, but none of
the test cases actually reach the new code, so adding Asserts doesn't help
to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't

mention the fact that this is a bug fix. Bug fixes should be clearly
documented as such, otherwise future work might assume the commit can be
reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or

have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with

more regression test coverage.

For reference, I said something similar earlier today in another

email to this thread:

This patch introduces a change that stores a new page into variable

"rightpage" rather than overwriting "state->target", which the old
implementation most certainly did. That means that after returning from
bt_target_page_check() into the calling function
bt_check_level_from_leftmost() the value in state->target is not what it
would have been prior to this patch. Now, that'd be irrelevant if nobody
goes on to consult that value, but just 44 lines further down in
bt_check_level_from_leftmost() state->target is clearly used. So the
behavior at that point is changing between the old and new versions of the
code, and I think I'm within reason to ask if it was wrong before the
patch, wrong after the patch, or something else? Is this a bug being
introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

I agree with your analysis regarding state->target:
- when the unique check is on, state->target was reassigned only for the
leaf pages (under P_ISLEAF(topaque) in bt_target_page_check).
- in this level (leaf) in bt_check_level_from_leftmost() this value of
state->target was used to get state->lowkey. Then it was reset (in the next
iteration of do loop in in bt_check_level_from_leftmost()
- state->lowkey lives until the end of pages level (leaf) iteration
cycle. Then, low-key is reset (state->lowkey = NULL in the end of
bt_check_level_from_leftmost())
- state->lowkey is used only in bt_child_check/bt_child_highkey_check.
Both are called only from non-leaf pages iteration cycles (under
P_ISLEAF(topaque))
- Also there is a check (rightblock_number != P_NONE) in before getting
rightpage into state->target in bt_target_page_check() that ensures us that
rightpage indeed exists and getting this (unused) lowkey in
bt_check_level_from_leftmost will not invoke any page reading errors.

I'm pretty sure that there was no bug in this, not just the bug was
hidden.

Indeed re-assigning state->target in leaf page iteration for cross-page
unique check was not beautiful, and Peter pointed out this. In my opinion
the patch 0003 is a pure code refactoring.

As for the cross-page check regression/TAP testing, this test had
problems since the btree page layout is not fixed (especially it's
different on 32-bit arch). I had a variant for testing cross-page check
when the test was yet regression one upthread for both 32/64 bit
architectures. I remember it was decided not to include it due to
complications and low impact for testing the corner case of very rare
cross-page duplicates. (There were also suggestions to drop cross-page
duplicates check at all, which I didn't agree 2 years ago, but still it can
make sense)

Separately, I propose to avoid getting state->lowkey for leaf pages at
all as it's unused. PFA is a simple patch for this. (I don't add it to the
current patch set as I believe it has nothing to do with UNIQUE constraint
check, rather it improves the previous btree amcheck code)

A correction of a typo in previous message:
non-leaf pages iteration cycles (under !P_ISLEAF(topaque)) -> non-leaf
pages iteration cycles (under !P_ISLEAF(topaque))

#72Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#68)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Mon, May 13, 2024 at 4:42 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <aekorotkov@gmail.com> wrote:
The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before bt_check_level_from_leftmost() overrides state->target in the next iteration, bt_check_level_from_leftmost() conditionally fetches an item from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses "rightpage" rather than "state->target" in the same iteration where bt_check_level_from_leftmost() conditionally fetches an item from state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target in an inappropriate way, causing the wrong thing to happen at what is now line 963. The patch fixes the bug, because state->target no longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong to wrong.

If you believe (1) is true, then I'm complaining that you are relying far to much on action at a distance, and that you are not documenting it. Even with documentation of this interrelationship, I'd be unhappy with how brittle the code is. I cannot easily discern that the two don't ever happen in the same iteration, and I'm not at all convinced one way or the other. I tried to set up some Asserts about that, but none of the test cases actually reach the new code, so adding Asserts doesn't help to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't mention the fact that this is a bug fix. Bug fixes should be clearly documented as such, otherwise future work might assume the commit can be reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

For reference, I said something similar earlier today in another email to this thread:

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

So, the picture for the patches is the following now.
0001 – optimization, but rather simple and giving huge effect
0002 – refactoring
0003 – fix for the bug
0004 – better error reporting

I think the thread contains enough motivation on why 0002, 0003 and
0004 are material for post-FF. They are fixes and refactoring for
new-in-v17 feature. I'm going to push them if no objections.

Regarding 0001, I'd like to ask Tom and Mark if they find convincing
that given that optimization is small, simple and giving huge effect,
it could be pushed post-FF? Otherwise, this could wait for v18.

------
Regards,
Alexander Korotkov
Supabase

#73Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Alexander Korotkov (#72)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Alexander!

On Fri, 17 May 2024 at 14:11, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Mon, May 13, 2024 at 4:42 AM Alexander Korotkov <aekorotkov@gmail.com>
wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <

aekorotkov@gmail.com> wrote:

The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in

the

next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before

bt_check_level_from_leftmost() overrides state->target in the next
iteration, bt_check_level_from_leftmost() conditionally fetches an item
from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses

"rightpage" rather than "state->target" in the same iteration where
bt_check_level_from_leftmost() conditionally fetches an item from
state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed

state->target in an inappropriate way, causing the wrong thing to happen at
what is now line 963. The patch fixes the bug, because state->target no
longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in

the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from

wrong to wrong.

If you believe (1) is true, then I'm complaining that you are

relying far to much on action at a distance, and that you are not
documenting it. Even with documentation of this interrelationship, I'd be
unhappy with how brittle the code is. I cannot easily discern that the two
don't ever happen in the same iteration, and I'm not at all convinced one
way or the other. I tried to set up some Asserts about that, but none of
the test cases actually reach the new code, so adding Asserts doesn't help
to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't

mention the fact that this is a bug fix. Bug fixes should be clearly
documented as such, otherwise future work might assume the commit can be
reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or

have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with

more regression test coverage.

For reference, I said something similar earlier today in another

email to this thread:

This patch introduces a change that stores a new page into variable

"rightpage" rather than overwriting "state->target", which the old
implementation most certainly did. That means that after returning from
bt_target_page_check() into the calling function
bt_check_level_from_leftmost() the value in state->target is not what it
would have been prior to this patch. Now, that'd be irrelevant if nobody
goes on to consult that value, but just 44 lines further down in
bt_check_level_from_leftmost() state->target is clearly used. So the
behavior at that point is changing between the old and new versions of the
code, and I think I'm within reason to ask if it was wrong before the
patch, wrong after the patch, or something else? Is this a bug being
introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

So, the picture for the patches is the following now.
0001 – optimization, but rather simple and giving huge effect
0002 – refactoring
0003 – fix for the bug
0004 – better error reporting

I think the thread contains enough motivation on why 0002, 0003 and
0004 are material for post-FF. They are fixes and refactoring for
new-in-v17 feature. I'm going to push them if no objections.

Regarding 0001, I'd like to ask Tom and Mark if they find convincing
that given that optimization is small, simple and giving huge effect,
it could be pushed post-FF? Otherwise, this could wait for v18.

In my view, patches 0002-0004 are worth pushing.
0001 is ready in my view. But I see no problem pushing it into v18
regarding that this optimization could be not eligible for post-FF. I don't
know the criteria for this just let's be safe about it.

Regards,
Pavel Borisov

#74Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Alexander Korotkov (#72)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 17, 2024, at 3:11 AM, Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Mon, May 13, 2024 at 4:42 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

On Mon, May 13, 2024 at 12:23 AM Alexander Korotkov
<aekorotkov@gmail.com> wrote:

On Sat, May 11, 2024 at 4:13 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On May 10, 2024, at 12:05 PM, Alexander Korotkov <aekorotkov@gmail.com> wrote:
The only bt_target_page_check() caller is
bt_check_level_from_leftmost(), which overrides state->target in the
next iteration anyway. I think the patch is just refactoring to
eliminate the confusion pointer by Peter Geoghegan upthread.

I find your argument unconvincing.

After bt_target_page_check() returns at line 919, and before bt_check_level_from_leftmost() overrides state->target in the next iteration, bt_check_level_from_leftmost() conditionally fetches an item from the page referenced by state->target. See line 963.

I'm left with four possibilities:

1) bt_target_page_check() never gets to the code that uses "rightpage" rather than "state->target" in the same iteration where bt_check_level_from_leftmost() conditionally fetches an item from state->target, so the change you're making doesn't matter.

2) The code prior to v2-0003 was wrong, having changed state->target in an inappropriate way, causing the wrong thing to happen at what is now line 963. The patch fixes the bug, because state->target no longer gets overwritten where you are now using "rightpage" for the value.

3) The code used to work, having set up state->target correctly in the place where you are now using "rightpage", but v2-0003 has broken that.

4) It's been broken all along and your patch just changes from wrong to wrong.

If you believe (1) is true, then I'm complaining that you are relying far to much on action at a distance, and that you are not documenting it. Even with documentation of this interrelationship, I'd be unhappy with how brittle the code is. I cannot easily discern that the two don't ever happen in the same iteration, and I'm not at all convinced one way or the other. I tried to set up some Asserts about that, but none of the test cases actually reach the new code, so adding Asserts doesn't help to investigate the question.

If (2) is true, then I'm complaining that the commit message doesn't mention the fact that this is a bug fix. Bug fixes should be clearly documented as such, otherwise future work might assume the commit can be reverted with only stylistic consequences.

If (3) is true, then I'm complaining that the patch is flat busted.

If (4) is true, then maybe we should revert the entire feature, or have a discussion of mitigation efforts that are needed.

Regardless of which of 1..4 you pick, I think it could all do with more regression test coverage.

For reference, I said something similar earlier today in another email to this thread:

This patch introduces a change that stores a new page into variable "rightpage" rather than overwriting "state->target", which the old implementation most certainly did. That means that after returning from bt_target_page_check() into the calling function bt_check_level_from_leftmost() the value in state->target is not what it would have been prior to this patch. Now, that'd be irrelevant if nobody goes on to consult that value, but just 44 lines further down in bt_check_level_from_leftmost() state->target is clearly used. So the behavior at that point is changing between the old and new versions of the code, and I think I'm within reason to ask if it was wrong before the patch, wrong after the patch, or something else? Is this a bug being introduced, being fixed, or ... ?

Thank you for your analysis. I'm inclined to believe in 2, but not
yet completely sure. It's really pity that our tests don't cover
this. I'm investigating this area.

It seems that I got to the bottom of this. Changing
BtreeCheckState.target for a cross-page unique constraint check is
wrong, but that happens only for leaf pages. After that
BtreeCheckState.target is only used for setting the low key. The low
key is only used for non-leaf pages. So, that didn't lead to any
visible bug. I've revised the commit message to reflect this.

So, the picture for the patches is the following now.
0001 – optimization, but rather simple and giving huge effect
0002 – refactoring
0003 – fix for the bug
0004 – better error reporting

I think the thread contains enough motivation on why 0002, 0003 and
0004 are material for post-FF. They are fixes and refactoring for
new-in-v17 feature. I'm going to push them if no objections.

Regarding 0001, I'd like to ask Tom and Mark if they find convincing
that given that optimization is small, simple and giving huge effect,
it could be pushed post-FF? Otherwise, this could wait for v18.

I won't pretend to be part of the Release Management Team. Perhaps Tom wishes to respond.

I wrote a TAP test to check the uniqueness checker. bt_index_check() sometimes fails to detect a corruption. This is true both before and after applying v3-0001. The bt_index_parent_check() seems to always detect the corruption created by the TAP test. Likewise, this is true both before and after applying v3-0001.

The documentation in https://www.postgresql.org/docs/devel/amcheck.html#AMCHECK-FUNCTIONS is ambiguous:

"bt_index_check does not verify invariants that span child/parent relationships, but will verify the presence of all heap tuples as index tuples within the index when heapallindexed is true. When checkunique is true bt_index_check will check that no more than one among duplicate entries in unique index is visible. When a routine, lightweight test for corruption is required in a live production environment, using bt_index_check often provides the best trade-off between thoroughness of verification and limiting the impact on application performance and availability."

The second sentence, "When checkunique is true bt_index_check will check that no more than one among duplicate entries in unique index is visible." is not strictly true, as it won't check if the violation spans a page boundary. That's implied by the surrounding sentences, but I'm not sure a reader can be trusted to know which way to interpret how "checkunique" works. Clarification is needed.

The attached TAP test is not intended for commit. I am only including it here because you might want to use the TAP test as a starting point for creating and testing for new kinds of corruption. Beware the test intentionally includes an infinite loop, which is helpful for a developer examining the code, but not at all appropriate otherwise. It loads all blocks of the index into memory each loop, which could be made more efficient if we wanted this to be part of the core codebase. I just threw it together this morning. It's not polished, documented, checked for portability, or otherwise production quality.

Attachments:

v1-0001-Add-a-WIP-corruption-checker.patchapplication/octet-stream; name=v1-0001-Add-a-WIP-corruption-checker.patch; x-unix-mode=0644Download
From cc7bf7991ff827c4b41c0cd7888a073b91f66827 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Fri, 17 May 2024 10:24:11 -0700
Subject: [PATCH v1] Add a WIP corruption checker

To help analyze Alexander Korotkov's v3 series of patches, add a
corruption checker that runs an infinite loop corrupting an index
and seeing if the corruption is detected.

THIS IS NOT FOR COMMIT.
---
 contrib/amcheck/t/006_corrupt_idx.pl | 136 +++++++++++++++++++++++++++
 1 file changed, 136 insertions(+)
 create mode 100644 contrib/amcheck/t/006_corrupt_idx.pl

diff --git a/contrib/amcheck/t/006_corrupt_idx.pl b/contrib/amcheck/t/006_corrupt_idx.pl
new file mode 100644
index 0000000000..d27b0ff9b6
--- /dev/null
+++ b/contrib/amcheck/t/006_corrupt_idx.pl
@@ -0,0 +1,136 @@
+
+# Copyright (c) 2023-2024, PostgreSQL Global Development Group
+
+# This regression test checks the behavior of the btree validation in the
+# presence of breaking sort order changes.
+#
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Fcntl 'SEEK_SET';
+
+my $node = PostgreSQL::Test::Cluster->new('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum = off');
+$node->start;
+
+# Create tables and indexes over some values
+$node->safe_psql('postgres',qq(
+CREATE EXTENSION amcheck;
+CREATE TABLE tbl (i TEXT);
+INSERT INTO tbl (SELECT 'MAGIC_' || gs::TEXT FROM generate_series(1,100000) gs ORDER BY RANDOM());
+CREATE UNIQUE INDEX idx ON tbl (i);
+));	
+
+my @blocks;
+
+while (1)
+{
+	my ($result, $stdout, $stderr);
+
+	$result = $node->safe_psql(
+		'postgres', q(
+		SELECT bt_index_parent_check('idx', true, true, true);
+	));
+	is($result, '', 'run amcheck on non-broken idx');
+
+	my $pgdata = $node->data_dir;
+	my $rel = $node->safe_psql('postgres',
+		qq(SELECT pg_relation_filepath('public.idx')));
+	my $relpath = "$pgdata/$rel";
+	$node->stop;
+
+	my ($blksize, @blocks) = read_blocks($relpath);
+
+	my $ttl = 1000;
+	my $corrupted_blkno = undef;
+	while (!defined($corrupted_blkno) && $ttl--)
+	{
+		my $blkno = int(rand(scalar(@blocks)-1));
+
+		if ($blocks[$blkno] =~ m/.*MAGIC_(\d+)/)
+		{
+			my $magic = $1;
+			my $corrupted_block = $blocks[$blkno];
+
+			my $next_magic = $magic + 1;
+			my $prev_magic = $magic - 1;
+			if ($next_magic >= 1 && $next_magic <= 100000 && $blocks[$blkno+1] =~ m/MAGIC_$next_magic/)
+			{
+				if ($corrupted_block =~ s/MAGIC_$magic/MAGIC_$next_magic/)
+				{
+					write_block($relpath, $blksize, $blkno, $corrupted_block);
+					$corrupted_blkno = $blkno;
+				}
+			}
+			elsif ($prev_magic >= 1 && $prev_magic <= 100000 && $blocks[$blkno+1] =~ m/MAGIC_$prev_magic/)
+			{
+				if ($corrupted_block =~ s/MAGIC_$magic/MAGIC_$prev_magic/)
+				{
+					write_block($relpath, $blksize, $blkno, $corrupted_block);
+					$corrupted_blkno = $blkno;
+				}
+			}
+		}
+	}
+
+	BAIL_OUT("Failed to corrupt anything") unless($ttl > 0);
+
+	# Ok, we've corrupted the file.  Restart the node and see if the
+	# corruption checker notices anything.
+	$node->start;
+
+	($result, $stdout, $stderr) = $node->psql(
+		'postgres', q(
+		SELECT bt_index_parent_check('idx', true, true, true);
+	));
+	ok( $stderr =~ /item order invariant violated for index "idx"|index uniqueness is violated for index "idx"|could not find tuple using search from root page in index "idx"|mismatch between parent key and child high key in index "idx"|detected uniqueness violation for index "idx"/);
+
+	# Repair the damage.
+	$node->stop;
+	write_block($relpath, $blksize, $corrupted_blkno, $blocks[$corrupted_blkno]);
+
+	# Restart the database and confirm the index is back to passing
+	$node->start;
+ 	$result = $node->safe_psql(
+		'postgres', q(
+		SELECT bt_index_check('idx', true, true);
+	));
+	is($result, '', 'run amcheck on non-broken idx');
+}
+
+# Not reached
+done_testing();
+
+sub read_blocks
+{
+	my ($relpath) = @_;
+	my $file;
+	open($file, '+<', $relpath)
+		or BAIL_OUT("open failed: $!");
+	binmode $file;
+	my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($file);
+
+	my @result;
+	for (my $blkno = 0; $blkno < $blocks; $blkno++)
+	{
+		sysseek($file, $blkno * $blksize, SEEK_SET);
+		sysread($file, $result[$blkno], $blksize);
+	}
+	close($file);
+	return ($blksize, @result);
+}
+
+sub write_block
+{
+	my ($relpath, $blksize, $blkno, $block) = @_;
+	my $file;
+	open($file, '+<', $relpath)
+		or BAIL_OUT("open failed: $!");
+	binmode $file;
+	sysseek($file, $blkno * $blksize, SEEK_SET);
+	syswrite($file, $block);
+	close($file);
+}
-- 
2.39.3 (Apple Git-145)

#75Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#74)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Mark!

The documentation in
https://www.postgresql.org/docs/devel/amcheck.html#AMCHECK-FUNCTIONS is
ambiguous:

"bt_index_check does not verify invariants that span child/parent
relationships, but will verify the presence of all heap tuples as index
tuples within the index when heapallindexed is true. When checkunique is
true bt_index_check will check that no more than one among duplicate
entries in unique index is visible. When a routine, lightweight test for
corruption is required in a live production environment, using
bt_index_check often provides the best trade-off between thoroughness of
verification and limiting the impact on application performance and
availability."

The second sentence, "When checkunique is true bt_index_check will check
that no more than one among duplicate entries in unique index is visible."
is not strictly true, as it won't check if the violation spans a page
boundary.

Amcheck with checkunique option does check uniqueness violation between
pages. But it doesn't warranty detection of cross page uniqueness
violations in extremely rare cases when the first equal index entry on the
next page corresponds to tuple that is not visible (e.g. dead). In this, I
followed the Peter's notion [1]/messages/by-id/CAH2-Wz=ttG__BTZ-r5ccopBRb5evjg=zsF_o_3C5h4zRBA_LjQ@mail.gmail.com that checking across a number of dead equal
entries that could theoretically span even across many pages is an
unneeded code complication and amcheck is not a tool that provides any
warranty when checking an index.

I'm not against docs modification in any way that clarifies its exact usage
and limitations.

Kind regards,
Pavel Borisov

[1]: /messages/by-id/CAH2-Wz=ttG__BTZ-r5ccopBRb5evjg=zsF_o_3C5h4zRBA_LjQ@mail.gmail.com
/messages/by-id/CAH2-Wz=ttG__BTZ-r5ccopBRb5evjg=zsF_o_3C5h4zRBA_LjQ@mail.gmail.com

#76Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#75)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 17, 2024, at 11:51 AM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

Amcheck with checkunique option does check uniqueness violation between pages. But it doesn't warranty detection of cross page uniqueness violations in extremely rare cases when the first equal index entry on the next page corresponds to tuple that is not visible (e.g. dead). In this, I followed the Peter's notion [1] that checking across a number of dead equal entries that could theoretically span even across many pages is an unneeded code complication and amcheck is not a tool that provides any warranty when checking an index.

This confuses me a bit. The regression test creates a table and index but never performs any DELETE nor any UPDATE operations, so none of the index entries should be dead. If I am understanding you correct, I'd be forced to conclude that the uniqueness checking code is broken. Can you take a look?


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#77Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Mark Dilger (#76)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 17, 2024, at 12:10 PM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:

Amcheck with checkunique option does check uniqueness violation between pages. But it doesn't warranty detection of cross page uniqueness violations in extremely rare cases when the first equal index entry on the next page corresponds to tuple that is not visible (e.g. dead). In this, I followed the Peter's notion [1] that checking across a number of dead equal entries that could theoretically span even across many pages is an unneeded code complication and amcheck is not a tool that provides any warranty when checking an index.

This confuses me a bit. The regression test creates a table and index but never performs any DELETE nor any UPDATE operations, so none of the index entries should be dead. If I am understanding you correct, I'd be forced to conclude that the uniqueness checking code is broken. Can you take a look?

On further review, the test was not anticipating the error message "high key invariant violated for index". That wasn't seen in calls to bt_index_parent_check(), but appears as one of the errors from bt_index_check(). I am rerunning the test now....


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#78Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#76)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Mark!

On Fri, 17 May 2024 at 23:10, Mark Dilger <mark.dilger@enterprisedb.com>
wrote:

On May 17, 2024, at 11:51 AM, Pavel Borisov <pashkin.elfe@gmail.com>

wrote:

Amcheck with checkunique option does check uniqueness violation between

pages. But it doesn't warranty detection of cross page uniqueness
violations in extremely rare cases when the first equal index entry on the
next page corresponds to tuple that is not visible (e.g. dead). In this, I
followed the Peter's notion [1] that checking across a number of dead equal
entries that could theoretically span even across many pages is an unneeded
code complication and amcheck is not a tool that provides any warranty when
checking an index.

This confuses me a bit. The regression test creates a table and index but
never performs any DELETE nor any UPDATE operations, so none of the index
entries should be dead. If I am understanding you correct, I'd be forced
to conclude that the uniqueness checking code is broken. Can you take a
look?

At the first glance it's not clear to me:
- why your test creates cross-page unique constraint violations?
- how do you know they are not detected?

In reply to: Mark Dilger (#77)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, May 17, 2024 at 3:42 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

On further review, the test was not anticipating the error message "high key invariant violated for index". That wasn't seen in calls to bt_index_parent_check(), but appears as one of the errors from bt_index_check(). I am rerunning the test now....

Many different parts of the B-Tree code will fight against allowing
duplicates of the same value to span multiple leaf pages -- this is
especially true for unique indexes. For example, nbtsplitloc.c has a
variety of strategies that will prevent choosing a split point that
necessitates including a distinguishing heap TID in the new high key.
In other words, nbtsplitloc.c is very aggressive about picking a split
point between (rather than within) groups of duplicates.

Of course it's still *possible* for a unique index to have multiple
leaf pages containing the same individual value. The regression tests
do have coverage for certain relevant code paths (e.g., there is
coverage for code paths only hit when _bt_check_unique has to go to
the page to the right). This is only the case because I went out of my
way to make sure of it, by adding tests that allow a huge number of
version duplicates to accumulate within a unique index. (The "move
right" _bt_check_unique branches had zero test coverage for a year or
two.)

Just how important it is that amcheck covers cases where the version
duplicates span multiple leaf pages is of course debatable -- it's
always better to be more thorough, when practical. But it's certainly
something that needs to be assessed based on the merits.

--
Peter Geoghegan

#80Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Pavel Borisov (#78)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 17, 2024, at 12:42 PM, Pavel Borisov <pashkin.elfe@gmail.com> wrote:

At the first glance it's not clear to me:
- why your test creates cross-page unique constraint violations?

To see if they are detected.

- how do you know they are not detected?

It appears that they are detected. At least, rerunning the test after adjusting the expected output, I no longer see problems.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#81Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Geoghegan (#79)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On May 17, 2024, at 1:00 PM, Peter Geoghegan <pg@bowt.ie> wrote:

Many different parts of the B-Tree code will fight against allowing
duplicates of the same value to span multiple leaf pages -- this is
especially true for unique indexes.

The quick-and-dirty TAP test I wrote this morning is intended to introduce duplicates across page boundaries, not to test for ones that got there by normal database activity. In other words, the TAP test forcibly corrupts the index by changing a value on one side of a boundary to be equal to the value on the other side of the boundary. Prior to the corrupting action the values were all unique.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

In reply to: Mark Dilger (#81)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, May 17, 2024 at 4:10 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:

The quick-and-dirty TAP test I wrote this morning is intended to introduce duplicates across page boundaries, not to test for ones that got there by normal database activity. In other words, the TAP test forcibly corrupts the index by changing a value on one side of a boundary to be equal to the value on the other side of the boundary. Prior to the corrupting action the values were all unique.

I understood that. I was just pointing out that an index that looks
even somewhat like that is already quite unnatural.

--
Peter Geoghegan

#83Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Mark Dilger (#80)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

Hi, Mark!

At the first glance it's not clear to me:
- why your test creates cross-page unique constraint violations?

To see if they are detected.

- how do you know they are not detected?

It appears that they are detected. At least, rerunning the test after
adjusting the expected output, I no longer see problems.

I understand your point. It was unclear how it modified the index so that
only unique constraint check between pages should have failed with other
checks passed.

Anyway, thanks for your testing and efforts! I'm happy that the test now
passes and confirms that amcheck feature works as intended.

Kind regards,
Pavel Borisov

#84Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alexander Korotkov (#72)
1 attachment(s)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, May 17, 2024 at 1:11 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

I think the thread contains enough motivation on why 0002, 0003 and
0004 are material for post-FF. They are fixes and refactoring for
new-in-v17 feature. I'm going to push them if no objections.

Regarding 0001, I'd like to ask Tom and Mark if they find convincing
that given that optimization is small, simple and giving huge effect,
it could be pushed post-FF? Otherwise, this could wait for v18.

The revised version of 0001 unique checking optimization is attached.
I'm going to push this to v18 if no objections.

------
Regards,
Alexander Korotkov
Supabase

Attachments:

v4-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchapplication/octet-stream; name=v4-0001-amcheck-Optimize-speed-of-checking-for-unique-con.patchDownload
From 324ab164ef7e32dd40e120bc22a79711a82cd77b Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 May 2024 03:07:53 +0300
Subject: [PATCH v4] amcheck: Optimize speed of checking for unique constraint
 violation

Currently, when amcheck validates a unique constraint, it visits the heap for
each index tuple.  This commit implements skipping keys, which have only one
non-dedeuplicated index tuple (quite common case for unique indexes). That
gives substantial economy on index checking time.

Reported-by: Noah Misch
Discussion: https://postgr.es/m/20240325020323.fd.nmisch%40google.com
Author: Alexander Korotkov, Pavel Borisov
---
 contrib/amcheck/verify_nbtree.c | 36 ++++++++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 3 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 34990c5cea3..7cfb136763f 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -1433,6 +1433,13 @@ bt_target_page_check(BtreeCheckState *state)
 		bool		lowersizelimit;
 		ItemPointer scantid;
 
+		/*
+		 * True if we already called bt_entry_unique_check() for the current
+		 * item.  This helps to avoid visiting the heap for keys, which are
+		 * anyway presented only once and can't comprise a unique violation.
+		 */
+		bool		unique_checked = false;
+
 		CHECK_FOR_INTERRUPTS();
 
 		itemid = PageGetItemIdCareful(state, state->targetblock,
@@ -1775,12 +1782,18 @@ bt_target_page_check(BtreeCheckState *state)
 
 		/*
 		 * If the index is unique verify entries uniqueness by checking the
-		 * heap tuples visibility.
+		 * heap tuples visibility.  Immediately check posting tuples and
+		 * tuples with repeated keys.  Postpone check for keys, which have the
+		 * first appearance.
 		 */
 		if (state->checkunique && state->indexinfo->ii_Unique &&
-			P_ISLEAF(topaque) && !skey->anynullkeys)
+			P_ISLEAF(topaque) && !skey->anynullkeys &&
+			(BTreeTupleIsPosting(itup) || ItemPointerIsValid(lVis.tid)))
+		{
 			bt_entry_unique_check(state, itup, state->targetblock, offset,
 								  &lVis);
+			unique_checked = true;
+		}
 
 		if (state->checkunique && state->indexinfo->ii_Unique &&
 			P_ISLEAF(topaque) && OffsetNumberNext(offset) <= max)
@@ -1799,6 +1812,9 @@ bt_target_page_check(BtreeCheckState *state)
 			 * data (whole index tuple or last posting in index tuple). Key
 			 * containing null value does not violate unique constraint and
 			 * treated as different to any other key.
+			 *
+			 * If the next key is the same as the previous one, do the
+			 * bt_entry_unique_check() call if it was postponed.
 			 */
 			if (_bt_compare(state->rel, skey, state->target,
 							OffsetNumberNext(offset)) != 0 || skey->anynullkeys)
@@ -1808,6 +1824,11 @@ bt_target_page_check(BtreeCheckState *state)
 				lVis.postingIndex = -1;
 				lVis.tid = NULL;
 			}
+			else if (!unique_checked)
+			{
+				bt_entry_unique_check(state, itup, state->targetblock, offset,
+									  &lVis);
+			}
 			skey->scantid = scantid;	/* Restore saved scan key state */
 		}
 
@@ -1890,10 +1911,19 @@ bt_target_page_check(BtreeCheckState *state)
 				rightkey->scantid = NULL;
 
 				/* The first key on the next page is the same */
-				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 && !rightkey->anynullkeys)
+				if (_bt_compare(state->rel, rightkey, state->target, max) == 0 &&
+					!rightkey->anynullkeys)
 				{
 					Page		rightpage;
 
+					/*
+					 * Do the bt_entry_unique_check() call if it was
+					 * postponed.
+					 */
+					if (!unique_checked)
+						bt_entry_unique_check(state, itup, state->targetblock,
+											  offset, &lVis);
+
 					elog(DEBUG2, "cross page equal keys");
 					rightpage = palloc_btree_page(state,
 												  rightblock_number);
-- 
2.39.3 (Apple Git-145)

#85Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#84)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, Jul 26, 2024 at 8:10 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

The revised version of 0001 unique checking optimization is attached.
I'm going to push this to v18 if no objections.

I have no reason to specifically object to pushing this into 18, but I
would like to point out that you're posting here about this but failed
to reply to the "64-bit pg_notify page numbers truncated to 32-bit",
an open item that was assigned to you but which, since you didn't
respond, was eventually fixed by commits from Michael Paquier.

I know it's easy to lose track of the open items list and I sometimes
forget to check it myself, but it's rather important to stay on top of
any open items that get assigned to you.

Thanks,

--
Robert Haas
EDB: http://www.enterprisedb.com

#86Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#85)
Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.

On Fri, Jul 26, 2024 at 5:38 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Fri, Jul 26, 2024 at 8:10 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:

The revised version of 0001 unique checking optimization is attached.
I'm going to push this to v18 if no objections.

I have no reason to specifically object to pushing this into 18, but I
would like to point out that you're posting here about this but failed
to reply to the "64-bit pg_notify page numbers truncated to 32-bit",
an open item that was assigned to you but which, since you didn't
respond, was eventually fixed by commits from Michael Paquier.

I know it's easy to lose track of the open items list and I sometimes
forget to check it myself, but it's rather important to stay on top of
any open items that get assigned to you.

Yes, it's a pity I miss this open item on me. Besides putting ashes
on my head, I think I could pay more attention on other open items.

------
Regards,
Alexander Korotkov
Supabase