public inbox for [email protected]
help / color / mirror / Atom feed[PATCH] xlog_block_info: show compression method
23+ messages / 11 participants
[nested] [flat]
* [PATCH] xlog_block_info: show compression method
@ 2022-02-19 17:35 Justin Pryzby <[email protected]>
0 siblings, 0 replies; 23+ messages in thread
From: Justin Pryzby @ 2022-02-19 17:35 UTC (permalink / raw)
---
src/backend/access/transam/xlogrecovery.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 4ad145dd167..75a72b47f78 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -2238,7 +2238,20 @@ xlog_block_info(StringInfo buf, XLogReaderState *record)
rlocator.relNumber,
blk);
if (XLogRecHasBlockImage(record, block_id))
- appendStringInfoString(buf, " FPW");
+ {
+ if (!BKPIMAGE_COMPRESSED(record->blocks[block_id].bimg_info))
+ appendStringInfoString(buf, " FPW");
+ else
+ {
+ uint8 info = record->blocks[block_id].bimg_info;
+ char *compression =
+ (BKPIMAGE_COMPRESS_PGLZ & info) ? "pglz" :
+ (BKPIMAGE_COMPRESS_LZ4 & info) ? "lz4" :
+ (BKPIMAGE_COMPRESS_ZSTD & info) ? "zstd" : "unknown";
+
+ appendStringInfo(buf, " FPW, compression method: %s", compression);
+ }
+ }
}
}
--
2.17.1
--um2V5WpqCyd73IVb--
^ permalink raw reply [nested|flat] 23+ messages in thread
* [PATCH] xlog_block_info: show compression method
@ 2022-02-19 17:35 Justin Pryzby <[email protected]>
0 siblings, 0 replies; 23+ messages in thread
From: Justin Pryzby @ 2022-02-19 17:35 UTC (permalink / raw)
---
src/backend/access/transam/xlogrecovery.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 4ad145dd167..75a72b47f78 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -2238,7 +2238,20 @@ xlog_block_info(StringInfo buf, XLogReaderState *record)
rlocator.relNumber,
blk);
if (XLogRecHasBlockImage(record, block_id))
- appendStringInfoString(buf, " FPW");
+ {
+ if (!BKPIMAGE_COMPRESSED(record->blocks[block_id].bimg_info))
+ appendStringInfoString(buf, " FPW");
+ else
+ {
+ uint8 info = record->blocks[block_id].bimg_info;
+ char *compression =
+ (BKPIMAGE_COMPRESS_PGLZ & info) ? "pglz" :
+ (BKPIMAGE_COMPRESS_LZ4 & info) ? "lz4" :
+ (BKPIMAGE_COMPRESS_ZSTD & info) ? "zstd" : "unknown";
+
+ appendStringInfo(buf, " FPW, compression method: %s", compression);
+ }
+ }
}
}
--
2.17.1
--um2V5WpqCyd73IVb--
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-02-21 14:14 Maxim Orlov <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Maxim Orlov @ 2022-02-21 14:14 UTC (permalink / raw)
To: Pavel Borisov <[email protected]>; +Cc: Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
I've updated the patch due to recent changes by Daniel Gustafsson
(549ec201d6132b7).
--
Best regards,
Maxim Orlov.
Attachments:
[application/octet-stream] v10-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.4K, ../../CACG=ezZho9PjoF5M+=hBqtfjur-jBmJ0b=GasQ4Yds9d+DUUgw@mail.gmail.com/3-v10-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From d23722b8f083ecfec69307e24c4748c9ddb09ff0 Mon Sep 17 00:00:00 2001
From: Maxim Orlov <[email protected]>
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 <[email protected]>
Pavel Borisov <[email protected]>
Maxim Orlov <[email protected]>
---
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-04-03 00:02 Greg Stark <[email protected]>
parent: Maxim Orlov <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Greg Stark @ 2022-04-03 00:02 UTC (permalink / raw)
To: Maxim Orlov <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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 <[email protected]> wrote:
>
> I've updated the patch due to recent changes by Daniel Gustafsson (549ec201d6132b7).
>
> --
> Best regards,
> Maxim Orlov.
--
greg
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-04-04 09:18 Pavel Borisov <[email protected]>
parent: Greg Stark <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Pavel Borisov @ 2022-04-04 09:18 UTC (permalink / raw)
To: Greg Stark <[email protected]>; +Cc: Maxim Orlov <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
>
> 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;
Attachments:
[application/octet-stream] v11-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.3K, ../../CALT9ZEFHWy9SG8BZCqqZMSvuSnRtzRG8ESM5tUxTDNBb-0qy6g@mail.gmail.com/3-v11-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From b2bae648a344bf42b874c41a5d633c949e7609f5 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Pavel Borisov <[email protected]>
Maxim Orlov <[email protected]>
---
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)
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-05-11 13:04 Pavel Borisov <[email protected]>
parent: Pavel Borisov <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Pavel Borisov @ 2022-05-11 13:04 UTC (permalink / raw)
To: Greg Stark <[email protected]>; +Cc: Maxim Orlov <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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;
Attachments:
[application/octet-stream] v12-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.5K, ../../CALT9ZEG9Zk5TxuDTnZPGe9H+b7DL40gXfeouWCaZ3A70yvyWLw@mail.gmail.com/3-v12-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From 097dbe8593d2b7d4229247c9e65e2136561c9fe8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Pavel Borisov <[email protected]>
Maxim Orlov <[email protected]>
---
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)
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-05-20 13:46 Pavel Borisov <[email protected]>
parent: Pavel Borisov <[email protected]>
0 siblings, 2 replies; 23+ messages in thread
From: Pavel Borisov @ 2022-05-20 13:46 UTC (permalink / raw)
To: Greg Stark <[email protected]>; +Cc: Maxim Orlov <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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;
Attachments:
[application/x-patch] v13-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (42.5K, ../../CALT9ZEG1YAfPBH-ZMnK-cxu0SDdrV_w0QEBs9BLHfWvddpOHFA@mail.gmail.com/3-v13-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From 21fe45c0ae8479e0733bd8caeb5d2a19d715e0d9 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Pavel Borisov <[email protected]>
Maxim Orlov <[email protected]>
---
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)
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-07-20 14:15 Aleksander Alekseev <[email protected]>
parent: Pavel Borisov <[email protected]>
1 sibling, 1 reply; 23+ messages in thread
From: Aleksander Alekseev @ 2022-07-20 14:15 UTC (permalink / raw)
To: Postgres hackers <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Mark Dilger <[email protected]>; Peter Geoghegan <[email protected]>
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:
[application/octet-stream] v14-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.3K, ../../CAJ7c6TP+Optg9TMZJd9+ic9R_c8RLU8DcUCyCoDnZZobb_1wQg@mail.gmail.com/2-v14-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From 37553352e796e877329e111b8744844ec5fb64ee Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-07 13:44 Dmitry Koval <[email protected]>
parent: Aleksander Alekseev <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Dmitry Koval @ 2022-09-07 13:44 UTC (permalink / raw)
To: Postgres hackers <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Mark Dilger <[email protected]>; Peter Geoghegan <[email protected]>; Aleksander Alekseev <[email protected]>
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
From 93a10abd0afb14b264e4cf59f7e92f619dd9b11a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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
Attachments:
[text/plain] v15-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.3K, ../../[email protected]/2-v15-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From 93a10abd0afb14b264e4cf59f7e92f619dd9b11a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-08 13:29 Karina Litskevich <[email protected]>
parent: Dmitry Koval <[email protected]>
0 siblings, 0 replies; 23+ messages in thread
From: Karina Litskevich @ 2022-09-08 13:29 UTC (permalink / raw)
To: Postgres hackers <[email protected]>; Pavel Borisov <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Mark Dilger <[email protected]>; Peter Geoghegan <[email protected]>; Aleksander Alekseev <[email protected]>; Dmitry Koval <[email protected]>
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:
[text/x-patch] v16-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.2K, ../../CACiT8iYi5HKksUenN2D+BxUGKvpRxuVdR37PWGwvDDea2OO6Tg@mail.gmail.com/3-v16-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From 56fe2b608b46c6c97900bbb63b2169e2997bc8cc Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-22 15:13 Andres Freund <[email protected]>
parent: Pavel Borisov <[email protected]>
1 sibling, 1 reply; 23+ messages in thread
From: Andres Freund @ 2022-09-22 15:13 UTC (permalink / raw)
To: Pavel Borisov <[email protected]>; +Cc: Greg Stark <[email protected]>; Maxim Orlov <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-27 08:04 Maxim Orlov <[email protected]>
parent: Andres Freund <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Maxim Orlov @ 2022-09-27 08:04 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
On Thu, 22 Sept 2022 at 18:13, Andres Freund <[email protected]> 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:
[application/octet-stream] v17-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch (43.8K, ../../CACG=ezbRVZrdTguWphiFP-L2cCi_jy+spZA0wPJWA806B0=2dQ@mail.gmail.com/3-v17-0001-Add-option-for-amcheck-and-pg_amcheck-to-check-u.patch)
download | inline diff:
From b7989f959429087328f7b1e521072567154d1167 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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)
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-28 08:36 Maxim Orlov <[email protected]>
parent: Maxim Orlov <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Maxim Orlov @ 2022-09-28 08:36 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Postgres hackers <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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.
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2022-09-28 08:43 Aleksander Alekseev <[email protected]>
parent: Maxim Orlov <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Aleksander Alekseev @ 2022-09-28 08:43 UTC (permalink / raw)
To: Postgres hackers <[email protected]>; +Cc: Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Hamid Akhtar <[email protected]>
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2023-10-24 20:13 Alexander Korotkov <[email protected]>
parent: Aleksander Alekseev <[email protected]>
0 siblings, 2 replies; 23+ messages in thread
From: Alexander Korotkov @ 2023-10-24 20:13 UTC (permalink / raw)
To: Aleksander Alekseev <[email protected]>; +Cc: Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Hamid Akhtar <[email protected]>
On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
<[email protected]> 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:
[application/octet-stream] 0001-Teach-contrib-amcheck-to-check-the-unique-constr-v18.patch (43.9K, ../../CAPpHfduqJ9QCOrXDjkPxPRxJRws1FgFELwB2Yz3L6-n7FQMsrA@mail.gmail.com/2-0001-Teach-contrib-amcheck-to-check-the-unique-constr-v18.patch)
download | inline diff:
From a405bdcbca38a9f44da70a20312a9bc81e50c76c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
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 <[email protected]>
Author: Pavel Borisov <[email protected]>
Author: Maxim Orlov <[email protected]>
Reviewed-by: Mark Dilger <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Peter Geoghegan <[email protected]>
Reviewed-by: Aleksander Alekseev <[email protected]>
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)
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2023-10-30 07:29 Pavel Borisov <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 0 replies; 23+ messages in thread
From: Pavel Borisov @ 2023-10-30 07:29 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Hamid Akhtar <[email protected]>
Hi, Alexander!
On Wed, 25 Oct 2023 at 00:13, Alexander Korotkov <[email protected]> wrote:
>
> On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
> <[email protected]> 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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-04-17 06:38 Peter Eisentraut <[email protected]>
parent: Alexander Korotkov <[email protected]>
1 sibling, 1 reply; 23+ messages in thread
From: Peter Eisentraut @ 2024-04-17 06:38 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; Aleksander Alekseev <[email protected]>; +Cc: Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]; Hamid Akhtar <[email protected]>
On 24.10.23 22:13, Alexander Korotkov wrote:
> On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
> <[email protected]> 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.
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-04-24 09:58 Alexander Korotkov <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 1 reply; 23+ messages in thread
From: Alexander Korotkov @ 2024-04-24 09:58 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Pavel Borisov <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
On Wed, Apr 17, 2024 at 9:38 AM Peter Eisentraut <[email protected]> wrote:
> On 24.10.23 22:13, Alexander Korotkov wrote:
> > On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
> > <[email protected]> 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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-04-25 12:59 Pavel Borisov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 2 replies; 23+ messages in thread
From: Pavel Borisov @ 2024-04-25 12:59 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
Hi, hackers!
On Wed, 24 Apr 2024 at 13:58, Alexander Korotkov <[email protected]>
wrote:
> On Wed, Apr 17, 2024 at 9:38 AM Peter Eisentraut <[email protected]>
> wrote:
> > On 24.10.23 22:13, Alexander Korotkov wrote:
> > > On Wed, Sep 28, 2022 at 11:44 AM Aleksander Alekseev
> > > <[email protected]> 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:
[application/octet-stream] v1-0004-Amcheck-Report-error-when-next-page-to-a-leaf-is-.patch (2.5K, ../../CALT9ZEFY-vsL4-cxta49wP=H7RjDaZk+Wg-ft4m-kVS_=WMsAQ@mail.gmail.com/3-v1-0004-Amcheck-Report-error-when-next-page-to-a-leaf-is-.patch)
download | inline diff:
From a2950357675198285dfdbc974346bce12dbd3c55 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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
[application/octet-stream] v1-0002-Amcheck-code-refactoring.patch (9.7K, ../../CALT9ZEFY-vsL4-cxta49wP=H7RjDaZk+Wg-ft4m-kVS_=WMsAQ@mail.gmail.com/4-v1-0002-Amcheck-code-refactoring.patch)
download | inline diff:
From 92a085925e84cd6c34c59861f2775e53a1048665 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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
[application/octet-stream] v1-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patch (1.7K, ../../CALT9ZEFY-vsL4-cxta49wP=H7RjDaZk+Wg-ft4m-kVS_=WMsAQ@mail.gmail.com/5-v1-0003-Amcheck-Don-t-load-rightpage-into-BtreeCheckState.patch)
download | inline diff:
From 526409e76419b387d9d4a40e94881231f256fcc8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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
[application/octet-stream] v1-0001-Amcheck-optimize-speed-of-checking-unique-constra.patch (2.5K, ../../CALT9ZEFY-vsL4-cxta49wP=H7RjDaZk+Wg-ft4m-kVS_=WMsAQ@mail.gmail.com/6-v1-0001-Amcheck-optimize-speed-of-checking-unique-constra.patch)
download | inline diff:
From 2847e83ef91bfa3e3ebda6a8acd60bd835950716 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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
[application/octet-stream] v1-0005-Rename-checkunique-parameter-for-amcheck-and-pg_a.patch (20.8K, ../../CALT9ZEFY-vsL4-cxta49wP=H7RjDaZk+Wg-ft4m-kVS_=WMsAQ@mail.gmail.com/7-v1-0005-Rename-checkunique-parameter-for-amcheck-and-pg_a.patch)
download | inline diff:
From ff4d989173084431914220d1a66646a332fa9d71 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <[email protected]>
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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-04-25 13:44 Karina Litskevich <[email protected]>
parent: Pavel Borisov <[email protected]>
1 sibling, 1 reply; 23+ messages in thread
From: Karina Litskevich @ 2024-04-25 13:44 UTC (permalink / raw)
To: Pavel Borisov <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Peter Eisentraut <[email protected]>; Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
Hi, hackers!
On Thu, Apr 25, 2024 at 4:00 PM Pavel Borisov <[email protected]>
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/
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-04-25 13:54 Pavel Borisov <[email protected]>
parent: Karina Litskevich <[email protected]>
0 siblings, 0 replies; 23+ messages in thread
From: Pavel Borisov @ 2024-04-25 13:54 UTC (permalink / raw)
To: Karina Litskevich <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Peter Eisentraut <[email protected]>; Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
Hi, Karina!
On Thu, 25 Apr 2024 at 17:44, Karina Litskevich <[email protected]>
wrote:
> Hi, hackers!
>
> On Thu, Apr 25, 2024 at 4:00 PM Pavel Borisov <[email protected]>
> 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
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-05-01 02:24 Noah Misch <[email protected]>
parent: Pavel Borisov <[email protected]>
1 sibling, 1 reply; 23+ messages in thread
From: Noah Misch @ 2024-05-01 02:24 UTC (permalink / raw)
To: Pavel Borisov <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Peter Eisentraut <[email protected]>; Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
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%.
^ permalink raw reply [nested|flat] 23+ messages in thread
* Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index.
@ 2024-05-01 02:26 Alexander Korotkov <[email protected]>
parent: Noah Misch <[email protected]>
0 siblings, 0 replies; 23+ messages in thread
From: Alexander Korotkov @ 2024-05-01 02:26 UTC (permalink / raw)
To: Noah Misch <[email protected]>; +Cc: Pavel Borisov <[email protected]>; Peter Eisentraut <[email protected]>; Aleksander Alekseev <[email protected]>; Postgres hackers <[email protected]>; Maxim Orlov <[email protected]>; Andres Freund <[email protected]>; Greg Stark <[email protected]>; Julien Rouhaud <[email protected]>; Mark Dilger <[email protected]>; David Steele <[email protected]>; Peter Geoghegan <[email protected]>; Maxim Orlov <[email protected]>; [email protected]
Hi Noah,
On Wed, May 1, 2024 at 5:24 AM Noah Misch <[email protected]> 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
^ permalink raw reply [nested|flat] 23+ messages in thread
end of thread, other threads:[~2024-05-01 02:26 UTC | newest]
Thread overview: 23+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2022-02-19 17:35 [PATCH] xlog_block_info: show compression method Justin Pryzby <[email protected]>
2022-02-19 17:35 [PATCH] xlog_block_info: show compression method Justin Pryzby <[email protected]>
2022-02-21 14:14 Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Maxim Orlov <[email protected]>
2022-04-03 00:02 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Greg Stark <[email protected]>
2022-04-04 09:18 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2022-05-11 13:04 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2022-05-20 13:46 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2022-07-20 14:15 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Aleksander Alekseev <[email protected]>
2022-09-07 13:44 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Dmitry Koval <[email protected]>
2022-09-08 13:29 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Karina Litskevich <[email protected]>
2022-09-22 15:13 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Andres Freund <[email protected]>
2022-09-27 08:04 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Maxim Orlov <[email protected]>
2022-09-28 08:36 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Maxim Orlov <[email protected]>
2022-09-28 08:43 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Aleksander Alekseev <[email protected]>
2023-10-24 20:13 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Alexander Korotkov <[email protected]>
2023-10-30 07:29 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2024-04-17 06:38 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Peter Eisentraut <[email protected]>
2024-04-24 09:58 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Alexander Korotkov <[email protected]>
2024-04-25 12:59 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2024-04-25 13:44 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Karina Litskevich <[email protected]>
2024-04-25 13:54 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Pavel Borisov <[email protected]>
2024-05-01 02:24 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Noah Misch <[email protected]>
2024-05-01 02:26 ` Re: [PATCH] Improve amcheck to also check UNIQUE constraint in btree index. Alexander Korotkov <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox