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