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

