diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile index 1b7a63cbaa40..1f2fec95de53 100644 --- a/contrib/amcheck/Makefile +++ b/contrib/amcheck/Makefile @@ -4,16 +4,17 @@ MODULE_big = amcheck OBJS = \ $(WIN32RES) \ verify_common.o \ + verify_gist.o \ verify_gin.o \ verify_heapam.o \ 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 \ - amcheck--1.3--1.4.sql amcheck--1.4--1.5.sql + amcheck--1.3--1.4.sql amcheck--1.4--1.5.sql amcheck--1.5--1.6.sql PGFILEDESC = "amcheck - function for verifying relation integrity" -REGRESS = check check_btree check_gin check_heap +REGRESS = check check_btree check_gin check_gist check_heap EXTRA_INSTALL = contrib/pg_walinspect TAP_TESTS = 1 diff --git a/contrib/amcheck/amcheck--1.5--1.6.sql b/contrib/amcheck/amcheck--1.5--1.6.sql new file mode 100644 index 000000000000..a6a1debff12c --- /dev/null +++ b/contrib/amcheck/amcheck--1.5--1.6.sql @@ -0,0 +1,14 @@ +/* contrib/amcheck/amcheck--1.5--1.6.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.6'" to load this file. \quit + + +-- gist_index_check() +-- +CREATE FUNCTION gist_index_check(index regclass, heapallindexed boolean) +RETURNS VOID +AS 'MODULE_PATHNAME', 'gist_index_check' +LANGUAGE C STRICT; + +REVOKE ALL ON FUNCTION gist_index_check(regclass,boolean) FROM PUBLIC; diff --git a/contrib/amcheck/amcheck.control b/contrib/amcheck/amcheck.control index c8ba6d7c9bc3..2f329ef2cf49 100644 --- a/contrib/amcheck/amcheck.control +++ b/contrib/amcheck/amcheck.control @@ -1,5 +1,5 @@ # amcheck extension comment = 'functions for verifying relation integrity' -default_version = '1.5' +default_version = '1.6' module_pathname = '$libdir/amcheck' relocatable = true diff --git a/contrib/amcheck/expected/check_gist.out b/contrib/amcheck/expected/check_gist.out new file mode 100644 index 000000000000..cbc3e27e6793 --- /dev/null +++ b/contrib/amcheck/expected/check_gist.out @@ -0,0 +1,145 @@ +SELECT setseed(1); + setseed +--------- + +(1 row) + +-- Test that index built with bulk load is correct +CREATE TABLE gist_check AS SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +CREATE INDEX gist_check_idx1 ON gist_check USING gist(c); +CREATE INDEX gist_check_idx2 ON gist_check USING gist(c) INCLUDE(p); +SELECT gist_index_check('gist_check_idx1', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx1', true); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', true); + gist_index_check +------------------ + +(1 row) + +-- Test that index is correct after inserts +INSERT INTO gist_check SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +SELECT gist_index_check('gist_check_idx1', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx1', true); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', true); + gist_index_check +------------------ + +(1 row) + +-- Test that index is correct after vacuuming +DELETE FROM gist_check WHERE c[1] < 5000; -- delete clustered data +DELETE FROM gist_check WHERE c[1]::int % 2 = 0; -- delete scattered data +-- We need two passes through the index and one global vacuum to actually +-- reuse page +VACUUM gist_check; +VACUUM; +SELECT gist_index_check('gist_check_idx1', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx1', true); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', true); + gist_index_check +------------------ + +(1 row) + +-- Test that index is correct after reusing pages +INSERT INTO gist_check SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +SELECT gist_index_check('gist_check_idx1', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', false); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx1', true); + gist_index_check +------------------ + +(1 row) + +SELECT gist_index_check('gist_check_idx2', true); + gist_index_check +------------------ + +(1 row) + +-- cleanup +DROP TABLE gist_check; +-- +-- Similar to BUG #15597 +-- +CREATE TABLE toast_bug(c point,buggy text); +ALTER TABLE toast_bug ALTER COLUMN buggy SET STORAGE extended; +CREATE INDEX toasty ON toast_bug USING gist(c) INCLUDE(buggy); +-- pg_attribute entry for toasty.buggy (the index) will have plain storage: +UPDATE pg_attribute SET attstorage = 'p' +WHERE attrelid = 'toasty'::regclass AND attname = 'buggy'; +-- Whereas pg_attribute entry for toast_bug.buggy (the table) still has extended storage: +SELECT attstorage FROM pg_attribute +WHERE attrelid = 'toast_bug'::regclass AND attname = 'buggy'; + attstorage +------------ + x +(1 row) + +-- Insert compressible heap tuple (comfortably exceeds TOAST_TUPLE_THRESHOLD): +INSERT INTO toast_bug SELECT point(0,0), repeat('a', 2200); +-- Should not get false positive report of corruption: +SELECT gist_index_check('toasty', true); + gist_index_check +------------------ + +(1 row) + diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build index 1f0c347ed541..13b36b495ed9 100644 --- a/contrib/amcheck/meson.build +++ b/contrib/amcheck/meson.build @@ -5,6 +5,7 @@ amcheck_sources = files( 'verify_gin.c', 'verify_heapam.c', 'verify_nbtree.c', + 'verify_gist.c', ) if host_system == 'windows' @@ -27,6 +28,7 @@ install_data( 'amcheck--1.2--1.3.sql', 'amcheck--1.3--1.4.sql', 'amcheck--1.4--1.5.sql', + 'amcheck--1.5--1.6.sql', kwargs: contrib_data_args, ) @@ -39,6 +41,7 @@ tests += { 'check', 'check_btree', 'check_gin', + 'check_gist', 'check_heap', ], }, diff --git a/contrib/amcheck/sql/check_gist.sql b/contrib/amcheck/sql/check_gist.sql new file mode 100644 index 000000000000..37966423b8b8 --- /dev/null +++ b/contrib/amcheck/sql/check_gist.sql @@ -0,0 +1,62 @@ + +SELECT setseed(1); + +-- Test that index built with bulk load is correct +CREATE TABLE gist_check AS SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +CREATE INDEX gist_check_idx1 ON gist_check USING gist(c); +CREATE INDEX gist_check_idx2 ON gist_check USING gist(c) INCLUDE(p); +SELECT gist_index_check('gist_check_idx1', false); +SELECT gist_index_check('gist_check_idx2', false); +SELECT gist_index_check('gist_check_idx1', true); +SELECT gist_index_check('gist_check_idx2', true); + +-- Test that index is correct after inserts +INSERT INTO gist_check SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +SELECT gist_index_check('gist_check_idx1', false); +SELECT gist_index_check('gist_check_idx2', false); +SELECT gist_index_check('gist_check_idx1', true); +SELECT gist_index_check('gist_check_idx2', true); + +-- Test that index is correct after vacuuming +DELETE FROM gist_check WHERE c[1] < 5000; -- delete clustered data +DELETE FROM gist_check WHERE c[1]::int % 2 = 0; -- delete scattered data + +-- We need two passes through the index and one global vacuum to actually +-- reuse page +VACUUM gist_check; +VACUUM; + +SELECT gist_index_check('gist_check_idx1', false); +SELECT gist_index_check('gist_check_idx2', false); +SELECT gist_index_check('gist_check_idx1', true); +SELECT gist_index_check('gist_check_idx2', true); + + +-- Test that index is correct after reusing pages +INSERT INTO gist_check SELECT point(random(),s) c, random() p FROM generate_series(1,10000) s; +SELECT gist_index_check('gist_check_idx1', false); +SELECT gist_index_check('gist_check_idx2', false); +SELECT gist_index_check('gist_check_idx1', true); +SELECT gist_index_check('gist_check_idx2', true); +-- cleanup +DROP TABLE gist_check; + +-- +-- Similar to BUG #15597 +-- +CREATE TABLE toast_bug(c point,buggy text); +ALTER TABLE toast_bug ALTER COLUMN buggy SET STORAGE extended; +CREATE INDEX toasty ON toast_bug USING gist(c) INCLUDE(buggy); + +-- pg_attribute entry for toasty.buggy (the index) will have plain storage: +UPDATE pg_attribute SET attstorage = 'p' +WHERE attrelid = 'toasty'::regclass AND attname = 'buggy'; + +-- Whereas pg_attribute entry for toast_bug.buggy (the table) still has extended storage: +SELECT attstorage FROM pg_attribute +WHERE attrelid = 'toast_bug'::regclass AND attname = 'buggy'; + +-- Insert compressible heap tuple (comfortably exceeds TOAST_TUPLE_THRESHOLD): +INSERT INTO toast_bug SELECT point(0,0), repeat('a', 2200); +-- Should not get false positive report of corruption: +SELECT gist_index_check('toasty', true); \ No newline at end of file diff --git a/contrib/amcheck/verify_gist.c b/contrib/amcheck/verify_gist.c new file mode 100644 index 000000000000..477150ac802e --- /dev/null +++ b/contrib/amcheck/verify_gist.c @@ -0,0 +1,687 @@ +/*------------------------------------------------------------------------- + * + * verify_gist.c + * Verifies the integrity of GiST indexes based on invariants. + * + * Verification checks that all paths in GiST graph contain + * consistent keys: tuples on parent pages consistently include tuples + * from children pages. Also, verification checks graph invariants: + * internal page must have at least one downlinks, internal page can + * reference either only leaf pages or only internal pages. + * + * + * Copyright (c) 2017-2023, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/amcheck/verify_gist.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/gist_private.h" +#include "access/tableam.h" +#include "catalog/index.h" +#include "catalog/pg_am.h" +#include "common/pg_prng.h" +#include "lib/bloomfilter.h" +#include "verify_common.h" +#include "utils/memutils.h" + + +/* + * GistScanItem represents one item of depth-first scan of GiST index. + */ +typedef struct GistScanItem +{ + int depth; + + /* Referenced block number to check next */ + BlockNumber blkno; + + /* + * Correctess of this parent tuple will be checked against contents of + * referenced page. This tuple will be NULL for root block. + */ + IndexTuple parenttup; + + /* + * LSN to hande concurrent scan of the page. It's necessary to avoid + * missing some subtrees from page, that was split just before we read it. + */ + XLogRecPtr parentlsn; + + /* + * Reference to parent page for re-locking in case of found parent-child + * tuple discrepencies. + */ + BlockNumber parentblk; + + /* Pointer to a next stack item. */ + struct GistScanItem *next; +} GistScanItem; + +typedef struct GistCheckState +{ + /* GiST state */ + GISTSTATE *state; + /* Bloom filter fingerprints index tuples */ + bloom_filter *filter; + + Snapshot snapshot; + Relation rel; + Relation heaprel; + + /* Debug counter for reporting percentage of work already done */ + int64 heaptuplespresent; + + /* progress reporting stuff */ + BlockNumber totalblocks; + BlockNumber reportedblocks; + BlockNumber scannedblocks; + BlockNumber deltablocks; + + int leafdepth; +} GistCheckState; + +PG_FUNCTION_INFO_V1(gist_index_check); + +static void giststate_init_heapallindexed(Relation rel, GistCheckState * result); +static void gist_check_parent_keys_consistency(Relation rel, Relation heaprel, + void *callback_state, bool readonly); +static void gist_check_page(GistCheckState * check_state, GistScanItem * stack, + Page page, bool heapallindexed, + BufferAccessStrategy strategy); +static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo); +static IndexTuple gist_refind_parent(Relation rel, BlockNumber parentblkno, + BlockNumber childblkno, + BufferAccessStrategy strategy); +static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, + Page page, OffsetNumber offset); +static void gist_tuple_present_callback(Relation index, ItemPointer tid, + Datum *values, bool *isnull, + bool tupleIsAlive, void *checkstate); +static IndexTuple gistFormNormalizedTuple(GISTSTATE *giststate, Relation r, + Datum *attdata, bool *isnull, ItemPointerData tid); + +/* + * gist_index_check(index regclass) + * + * Verify integrity of GiST index. + * + * Acquires AccessShareLock on heap & index relations. + */ +Datum +gist_index_check(PG_FUNCTION_ARGS) +{ + Oid indrelid = PG_GETARG_OID(0); + bool heapallindexed = PG_GETARG_BOOL(1); + + amcheck_lock_relation_and_check(indrelid, + GIST_AM_OID, + gist_check_parent_keys_consistency, + AccessShareLock, + &heapallindexed); + + PG_RETURN_VOID(); +} + +/* +* Initaliaze GIST state filed needed to perform. +* This initialized bloom filter and snapshot. +*/ +static void +giststate_init_heapallindexed(Relation rel, GistCheckState * result) +{ + int64 total_pages; + int64 total_elems; + uint64 seed; + + /* + * Size Bloom filter based on estimated number of tuples in index. This + * logic is similar to B-tree, see verify_btree.c . + */ + total_pages = result->totalblocks; + total_elems = Max(total_pages * (MaxOffsetNumber / 5), + (int64) rel->rd_rel->reltuples); + seed = pg_prng_uint64(&pg_global_prng_state); + result->filter = bloom_create(total_elems, maintenance_work_mem, seed); + + result->snapshot = RegisterSnapshot(GetTransactionSnapshot()); + + + /* + * GetTransactionSnapshot() always acquires a new MVCC snapshot in READ + * COMMITTED mode. A new snapshot is guaranteed to have all the entries + * it requires in the index. + * + * We must defend against the possibility that an old xact snapshot was + * returned at higher isolation levels when that snapshot is not safe for + * index scans of the target index. This is possible when the snapshot + * sees tuples that are before the index's indcheckxmin horizon. Throwing + * an error here should be very rare. It doesn't seem worth using a + * secondary snapshot to avoid this. + */ + if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin && + !TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data), + result->snapshot->xmin)) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("index \"%s\" cannot be verified using transaction snapshot", + RelationGetRelationName(rel)))); +} + +/* + * Main entry point for GiST check. + * + * This function verifies that tuples of internal pages cover all + * the key space of each tuple on leaf page. To do this we invoke + * gist_check_internal_page() for every internal page. + * + * This check allocates memory context and scans through + * GiST graph. This scan is performed in a depth-first search using a stack of + * GistScanItem-s. Initially this stack contains only root block number. On + * each iteration top block numbmer is replcaed by referenced block numbers. + * + * + * gist_check_internal_page() in it's turn takes every tuple and tries to + * adjust it by tuples on referenced child page. Parent gist tuple should + * never require any adjustments. + */ +static void +gist_check_parent_keys_consistency(Relation rel, Relation heaprel, + void *callback_state, bool readonly) +{ + BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD); + GistScanItem *stack; + MemoryContext mctx; + MemoryContext oldcontext; + GISTSTATE *state; + bool heapallindexed = *((bool *) callback_state); + GistCheckState *check_state = palloc0(sizeof(GistCheckState)); + + mctx = AllocSetContextCreate(CurrentMemoryContext, + "amcheck context", + ALLOCSET_DEFAULT_SIZES); + oldcontext = MemoryContextSwitchTo(mctx); + + state = initGISTstate(rel); + + check_state->state = state; + check_state->rel = rel; + check_state->heaprel = heaprel; + + /* + * We don't know the height of the tree yet, but as soon as we encounter a + * leaf page, we will set 'leafdepth' to its depth. + */ + check_state->leafdepth = -1; + + check_state->totalblocks = RelationGetNumberOfBlocks(rel); + /* report every 100 blocks or 5%, whichever is bigger */ + check_state->deltablocks = Max(check_state->totalblocks / 20, 100); + + if (heapallindexed) + giststate_init_heapallindexed(rel, check_state); + + /* Start the scan at the root page */ + stack = (GistScanItem *) palloc0(sizeof(GistScanItem)); + stack->depth = 0; + stack->parenttup = NULL; + stack->parentblk = InvalidBlockNumber; + stack->parentlsn = InvalidXLogRecPtr; + stack->blkno = GIST_ROOT_BLKNO; + + /* + * This GiST scan is effectively "old" VACUUM version before commit + * fe280694d which introduced physical order scanning. + */ + + while (stack) + { + GistScanItem *stack_next; + Buffer buffer; + Page page; + XLogRecPtr lsn; + + CHECK_FOR_INTERRUPTS(); + + /* Report progress */ + if (check_state->scannedblocks > check_state->reportedblocks + + check_state->deltablocks) + { + elog(DEBUG1, "verified level %u blocks of approximately %u total", + check_state->scannedblocks, check_state->totalblocks); + check_state->reportedblocks = check_state->scannedblocks; + } + check_state->scannedblocks++; + + buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, + RBM_NORMAL, strategy); + LockBuffer(buffer, GIST_SHARE); + page = (Page) BufferGetPage(buffer); + lsn = BufferGetLSNAtomic(buffer); + + /* Do basic sanity checks on the page headers */ + check_index_page(rel, buffer, stack->blkno); + + /* + * It's possible that the page was split since we looked at the + * parent, so that we didn't missed the downlink of the right sibling + * when we scanned the parent. If so, add the right sibling to the + * stack now. + */ + if (GistFollowRight(page) || stack->parentlsn < GistPageGetNSN(page)) + { + /* split page detected, install right link to the stack */ + GistScanItem *ptr = (GistScanItem *) palloc(sizeof(GistScanItem)); + + ptr->depth = stack->depth; + ptr->parenttup = CopyIndexTuple(stack->parenttup); + ptr->parentblk = stack->parentblk; + ptr->parentlsn = stack->parentlsn; + ptr->blkno = GistPageGetOpaque(page)->rightlink; + ptr->next = stack->next; + stack->next = ptr; + } + + gist_check_page(check_state, stack, page, heapallindexed, strategy); + + if (!GistPageIsLeaf(page)) + { + OffsetNumber maxoff = PageGetMaxOffsetNumber(page); + + for (OffsetNumber i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) + { + /* Internal page, so recurse to the child */ + GistScanItem *ptr; + ItemId iid = PageGetItemIdCareful(rel, stack->blkno, page, i); + IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid); + + ptr = (GistScanItem *) palloc(sizeof(GistScanItem)); + ptr->depth = stack->depth + 1; + ptr->parenttup = CopyIndexTuple(idxtuple); + ptr->parentblk = stack->blkno; + ptr->blkno = ItemPointerGetBlockNumber(&(idxtuple->t_tid)); + ptr->parentlsn = lsn; + ptr->next = stack->next; + stack->next = ptr; + } + } + + LockBuffer(buffer, GIST_UNLOCK); + ReleaseBuffer(buffer); + + /* Step to next item in the queue */ + stack_next = stack->next; + if (stack->parenttup) + pfree(stack->parenttup); + pfree(stack); + stack = stack_next; + } + + if (heapallindexed) + { + IndexInfo *indexinfo = BuildIndexInfo(rel); + TableScanDesc scan; + + scan = table_beginscan_strat(heaprel, /* relation */ + check_state->snapshot, /* snapshot */ + 0, /* number of keys */ + NULL, /* scan key */ + true, /* buffer access strategy OK */ + true); /* syncscan OK? */ + + /* + * Scan will behave as the first scan of a CREATE INDEX CONCURRENTLY. + */ + indexinfo->ii_Concurrent = true; + + indexinfo->ii_Unique = false; + indexinfo->ii_ExclusionOps = NULL; + indexinfo->ii_ExclusionProcs = NULL; + indexinfo->ii_ExclusionStrats = NULL; + + elog(DEBUG1, "verifying that tuples from index \"%s\" are present in \"%s\"", + RelationGetRelationName(rel), + RelationGetRelationName(heaprel)); + + table_index_build_scan(heaprel, rel, indexinfo, true, false, + gist_tuple_present_callback, (void *) check_state, scan); + + ereport(DEBUG1, + (errmsg_internal("finished verifying presence of " INT64_FORMAT " tuples from table \"%s\" with bitset %.2f%% set", + check_state->heaptuplespresent, + RelationGetRelationName(heaprel), + 100.0 * bloom_prop_bits_set(check_state->filter)))); + + UnregisterSnapshot(check_state->snapshot); + bloom_free(check_state->filter); + } + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(mctx); + pfree(check_state); +} + +static void +gist_check_page(GistCheckState * check_state, GistScanItem * stack, + Page page, bool heapallindexed, BufferAccessStrategy strategy) +{ + OffsetNumber maxoff = PageGetMaxOffsetNumber(page); + + /* Check that the tree has the same height in all branches */ + if (GistPageIsLeaf(page)) + { + if (check_state->leafdepth == -1) + check_state->leafdepth = stack->depth; + else if (stack->depth != check_state->leafdepth) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\": internal pages traversal encountered leaf page unexpectedly on block %u", + RelationGetRelationName(check_state->rel), stack->blkno))); + } + + /* + * Check that each tuple looks valid, and is consistent with the downlink + * we followed when we stepped on this page. + */ + for (OffsetNumber i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) + { + ItemId iid = PageGetItemIdCareful(check_state->rel, stack->blkno, page, i); + IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid); + IndexTuple tmpTuple = NULL; + + /* + * Check that it's not a leftover invalid tuple from pre-9.1 See also + * gistdoinsert() and gistbulkdelete() handling of such tuples. We do + * consider it error here. + */ + if (GistTupleIsInvalid(idxtuple)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("index \"%s\" contains an inner tuple marked as invalid, block %u, offset %u", + RelationGetRelationName(check_state->rel), stack->blkno, i), + errdetail("This is caused by an incomplete page split at crash recovery before upgrading to PostgreSQL 9.1."), + errhint("Please REINDEX it."))); + + if (MAXALIGN(ItemIdGetLength(iid)) != MAXALIGN(IndexTupleSize(idxtuple))) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has inconsistent tuple sizes, block %u, offset %u", + RelationGetRelationName(check_state->rel), stack->blkno, i))); + + /* + * Check if this tuple is consistent with the downlink in the parent. + */ + if (stack->parenttup) + tmpTuple = gistgetadjusted(check_state->rel, stack->parenttup, idxtuple, check_state->state); + + if (tmpTuple) + { + /* + * There was a discrepancy between parent and child tuples. We + * need to verify it is not a result of concurrent call of + * gistplacetopage(). So, lock parent and try to find downlink for + * current page. It may be missing due to concurrent page split, + * this is OK. + * + * Note that when we aquire parent tuple now we hold lock for both + * parent and child buffers. Thus parent tuple must include + * keyspace of the child. + */ + + pfree(tmpTuple); + pfree(stack->parenttup); + stack->parenttup = gist_refind_parent(check_state->rel, stack->parentblk, + stack->blkno, strategy); + + /* We found it - make a final check before failing */ + if (!stack->parenttup) + elog(NOTICE, "Unable to find parent tuple for block %u on block %u due to concurrent split", + stack->blkno, stack->parentblk); + else if (gistgetadjusted(check_state->rel, stack->parenttup, idxtuple, check_state->state)) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has inconsistent records on page %u offset %u", + RelationGetRelationName(check_state->rel), stack->blkno, i))); + else + { + /* + * But now it is properly adjusted - nothing to do here. + */ + } + } + + if (GistPageIsLeaf(page)) + { + if (heapallindexed) + bloom_add_element(check_state->filter, + (unsigned char *) idxtuple, + IndexTupleSize(idxtuple)); + } + else + { + OffsetNumber off = ItemPointerGetOffsetNumber(&(idxtuple->t_tid)); + + if (off != 0xffff) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has on page %u offset %u has item id not pointing to 0xffff, but %hu", + RelationGetRelationName(check_state->rel), stack->blkno, i, off))); + } + } +} + +/* + * gistFormNormalizedTuple - analogue to gistFormTuple, but performs deTOASTing + * of all included data (for covering indexes). While we do not expected + * toasted attributes in normal index, this can happen as a result of + * intervention into system catalog. Detoasting of key attributes is expected + * to be done by opclass decompression methods, if indexed type might be + * toasted. + */ +static IndexTuple +gistFormNormalizedTuple(GISTSTATE *giststate, Relation r, + Datum *attdata, bool *isnull, ItemPointerData tid) +{ + Datum compatt[INDEX_MAX_KEYS]; + IndexTuple res; + + gistCompressValues(giststate, r, attdata, isnull, true, compatt); + + for (int i = 0; i < r->rd_att->natts; i++) + { + Form_pg_attribute att; + + att = TupleDescAttr(giststate->leafTupdesc, i); + if (att->attbyval || att->attlen != -1 || isnull[i]) + continue; + + if (VARATT_IS_EXTERNAL(DatumGetPointer(compatt[i]))) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("external varlena datum in tuple that references heap row (%u,%u) in index \"%s\"", + ItemPointerGetBlockNumber(&tid), + ItemPointerGetOffsetNumber(&tid), + RelationGetRelationName(r)))); + if (VARATT_IS_COMPRESSED(DatumGetPointer(compatt[i]))) + { + /* Datum old = compatt[i]; */ + /* Key attributes must never be compressed */ + if (i < IndexRelationGetNumberOfKeyAttributes(r)) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("compressed varlena datum in tuple key that references heap row (%u,%u) in index \"%s\"", + ItemPointerGetBlockNumber(&tid), + ItemPointerGetOffsetNumber(&tid), + RelationGetRelationName(r)))); + + compatt[i] = PointerGetDatum(PG_DETOAST_DATUM(compatt[i])); + /* pfree(DatumGetPointer(old)); // TODO: this fails. Why? */ + } + } + + res = index_form_tuple(giststate->leafTupdesc, compatt, isnull); + + /* + * The offset number on tuples on internal pages is unused. For historical + * reasons, it is set to 0xffff. + */ + ItemPointerSetOffsetNumber(&(res->t_tid), 0xffff); + return res; +} + +static void +gist_tuple_present_callback(Relation index, ItemPointer tid, Datum *values, + bool *isnull, bool tupleIsAlive, void *checkstate) +{ + GistCheckState *state = (GistCheckState *) checkstate; + IndexTuple itup = gistFormNormalizedTuple(state->state, index, values, isnull, *tid); + + itup->t_tid = *tid; + /* Probe Bloom filter -- tuple should be present */ + if (bloom_lacks_element(state->filter, (unsigned char *) itup, + IndexTupleSize(itup))) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("heap tuple (%u,%u) from table \"%s\" lacks matching index tuple within index \"%s\"", + ItemPointerGetBlockNumber(&(itup->t_tid)), + ItemPointerGetOffsetNumber(&(itup->t_tid)), + RelationGetRelationName(state->heaprel), + RelationGetRelationName(state->rel)))); + + state->heaptuplespresent++; + + pfree(itup); +} + +/* + * check_index_page - verification of basic invariants about GiST page data + * This function does no any tuple analysis. + */ +static void +check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo) +{ + Page page = BufferGetPage(buffer); + + gistcheckpage(rel, buffer); + + if (GistPageGetOpaque(page)->gist_page_id != GIST_PAGE_ID) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has corrupted page %d", + RelationGetRelationName(rel), blockNo))); + + if (GistPageIsDeleted(page)) + { + if (!GistPageIsLeaf(page)) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has deleted internal page %d", + RelationGetRelationName(rel), blockNo))); + if (PageGetMaxOffsetNumber(page) > InvalidOffsetNumber) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has deleted page %d with tuples", + RelationGetRelationName(rel), blockNo))); + } + else if (PageGetMaxOffsetNumber(page) > MaxIndexTuplesPerPage) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" has page %d with exceeding count of tuples", + RelationGetRelationName(rel), blockNo))); +} + +/* + * Try to re-find downlink pointing to 'blkno', in 'parentblkno'. + * + * If found, returns a palloc'd copy of the downlink tuple. Otherwise, + * returns NULL. + */ +static IndexTuple +gist_refind_parent(Relation rel, + BlockNumber parentblkno, BlockNumber childblkno, + BufferAccessStrategy strategy) +{ + Buffer parentbuf; + Page parentpage; + OffsetNumber parent_maxoff; + IndexTuple result = NULL; + + parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL, + strategy); + + LockBuffer(parentbuf, GIST_SHARE); + parentpage = BufferGetPage(parentbuf); + + if (GistPageIsLeaf(parentpage)) + { + /* + * Currently GiST never deletes internal pages, thus they can never + * become leaf. + */ + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" internal page %d became leaf", + RelationGetRelationName(rel), parentblkno))); + } + + parent_maxoff = PageGetMaxOffsetNumber(parentpage); + for (OffsetNumber o = FirstOffsetNumber; o <= parent_maxoff; o = OffsetNumberNext(o)) + { + ItemId p_iid = PageGetItemIdCareful(rel, parentblkno, parentpage, o); + IndexTuple itup = (IndexTuple) PageGetItem(parentpage, p_iid); + + if (ItemPointerGetBlockNumber(&(itup->t_tid)) == childblkno) + { + /* + * Found it! Make copy and return it while both parent and child + * pages are locked. This guaranties that at this particular + * moment tuples must be coherent to each other. + */ + result = CopyIndexTuple(itup); + break; + } + } + + UnlockReleaseBuffer(parentbuf); + + return result; +} + +static ItemId +PageGetItemIdCareful(Relation rel, BlockNumber block, Page page, + OffsetNumber offset) +{ + ItemId itemid = PageGetItemId(page, offset); + + if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) > + BLCKSZ - MAXALIGN(sizeof(GISTPageOpaqueData))) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("line pointer points past end of tuple space in index \"%s\"", + RelationGetRelationName(rel)), + errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.", + block, offset, ItemIdGetOffset(itemid), + ItemIdGetLength(itemid), + ItemIdGetFlags(itemid)))); + + /* + * Verify that line pointer isn't LP_REDIRECT or LP_UNUSED, since gist + * never uses either. Verify that line pointer has storage, too, since + * even LP_DEAD items should. + */ + if (ItemIdIsRedirected(itemid) || !ItemIdIsUsed(itemid) || + ItemIdGetLength(itemid) == 0) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("invalid line pointer storage in index \"%s\"", + RelationGetRelationName(rel)), + errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.", + block, offset, ItemIdGetOffset(itemid), + ItemIdGetLength(itemid), + ItemIdGetFlags(itemid)))); + + return itemid; +} diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index 0aff0a6c8c6f..7e4b6c6f6927 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -208,6 +208,25 @@ ORDER BY c.relpages DESC LIMIT 10; + + + gist_index_check(index regclass, heapallindexed boolean) returns void + + gist_index_check + + + + + + gist_index_check tests that its target GiST + has consistent parent-child tuples relations (no parent tuples + require tuple adjustement) and page graph respects balanced-tree + invariants (internal pages reference only leaf page or only internal + pages). + + + + diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c index 2b1fd566c353..272d0fde7089 100644 --- a/src/bin/pg_amcheck/pg_amcheck.c +++ b/src/bin/pg_amcheck/pg_amcheck.c @@ -40,8 +40,7 @@ typedef struct PatternInfo * NULL */ bool heap_only; /* true if rel_regex should only match heap * tables */ - bool btree_only; /* true if rel_regex should only match btree - * indexes */ + bool index_only; /* true if rel_regex should only match indexes */ bool matched; /* true if the pattern matched in any database */ } PatternInfo; @@ -75,10 +74,9 @@ typedef struct AmcheckOptions /* * As an optimization, if any pattern in the exclude list applies to heap - * tables, or similarly if any such pattern applies to btree indexes, or - * to schemas, then these will be true, otherwise false. These should - * always agree with what you'd conclude by grep'ing through the exclude - * list. + * tables, or similarly if any such pattern applies to indexes, or to + * schemas, then these will be true, otherwise false. These should always + * agree with what you'd conclude by grep'ing through the exclude list. */ bool excludetbl; bool excludeidx; @@ -99,14 +97,14 @@ typedef struct AmcheckOptions int64 endblock; const char *skip; - /* btree index checking options */ + /* index checking options */ bool parent_check; bool rootdescend; bool heapallindexed; bool checkunique; - /* heap and btree hybrid option */ - bool no_btree_expansion; + /* heap and indexes hybrid option */ + bool no_index_expansion; } AmcheckOptions; static AmcheckOptions opts = { @@ -135,7 +133,7 @@ static AmcheckOptions opts = { .rootdescend = false, .heapallindexed = false, .checkunique = false, - .no_btree_expansion = false + .no_index_expansion = false }; static const char *progname = NULL; @@ -152,13 +150,16 @@ typedef struct DatabaseInfo char *datname; char *amcheck_schema; /* escaped, quoted literal */ bool is_checkunique; + bool gist_supported; + bool gin_supported; } DatabaseInfo; typedef struct RelationInfo { const DatabaseInfo *datinfo; /* shared by other relinfos */ Oid reloid; - bool is_heap; /* true if heap, false if btree */ + Oid amoid; + bool is_heap; /* true if heap, false if index */ char *nspname; char *relname; int relpages; @@ -179,10 +180,14 @@ static void prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn); static void prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn); +static void prepare_gist_command(PQExpBuffer sql, RelationInfo *rel, + PGconn *conn); +static void prepare_gin_command(PQExpBuffer sql, RelationInfo *rel, + PGconn *conn); static void run_command(ParallelSlot *slot, const char *sql); static bool verify_heap_slot_handler(PGresult *res, PGconn *conn, void *context); -static bool verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context); +static bool verify_index_slot_handler(PGresult *res, PGconn *conn, void *context); static void help(const char *progname); static void progress_report(uint64 relations_total, uint64 relations_checked, uint64 relpages_total, uint64 relpages_checked, @@ -196,7 +201,7 @@ static void append_relation_pattern(PatternInfoArray *pia, const char *pattern, int encoding); static void append_heap_pattern(PatternInfoArray *pia, const char *pattern, int encoding); -static void append_btree_pattern(PatternInfoArray *pia, const char *pattern, +static void append_index_pattern(PatternInfoArray *pia, const char *pattern, int encoding); static void compile_database_list(PGconn *conn, SimplePtrList *databases, const char *initial_dbname); @@ -288,6 +293,8 @@ main(int argc, char *argv[]) enum trivalue prompt_password = TRI_DEFAULT; int encoding = pg_get_encoding_from_locale(NULL, false); ConnParams cparams; + bool gist_warn_printed = false; + bool gin_warn_printed = false; pg_logging_init(argv[0]); progname = get_progname(argv[0]); @@ -323,11 +330,11 @@ main(int argc, char *argv[]) break; case 'i': opts.allrel = false; - append_btree_pattern(&opts.include, optarg, encoding); + append_index_pattern(&opts.include, optarg, encoding); break; case 'I': opts.excludeidx = true; - append_btree_pattern(&opts.exclude, optarg, encoding); + append_index_pattern(&opts.exclude, optarg, encoding); break; case 'j': if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX, @@ -382,7 +389,7 @@ main(int argc, char *argv[]) maintenance_db = pg_strdup(optarg); break; case 2: - opts.no_btree_expansion = true; + opts.no_index_expansion = true; break; case 3: opts.no_toast_expansion = true; @@ -531,6 +538,10 @@ main(int argc, char *argv[]) int ntups; const char *amcheck_schema = NULL; DatabaseInfo *dat = (DatabaseInfo *) cell->ptr; + int vmaj = 0, + vmin = 0, + vrev = 0; + const char *amcheck_version; cparams.override_dbname = dat->datname; if (conn == NULL || strcmp(PQdb(conn), dat->datname) != 0) @@ -600,36 +611,35 @@ main(int argc, char *argv[]) strlen(amcheck_schema)); /* - * Check the version of amcheck extension. Skip requested unique - * constraint check with warning if it is not yet supported by - * amcheck. + * Check the version of amcheck extension. */ - 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); + amcheck_version = PQgetvalue(result, 0, 1); - sscanf(amcheck_version, "%d.%d.%d", &vmaj, &vmin, &vrev); + /* + * 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. + */ + 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("option %s is not supported by amcheck version %s", - "--checkunique", amcheck_version); - dat->is_checkunique = false; - } - else - dat->is_checkunique = true; + /* + * checkunique option is supported in amcheck since version 1.4. Skip + * requested unique constraint check with warning if it is not yet + * supported by amcheck. + */ + if (opts.checkunique && ((vmaj == 1 && vmin < 4) || vmaj == 0)) + { + pg_log_warning("option %s is not supported by amcheck version %s", + "--checkunique", amcheck_version); + dat->is_checkunique = false; } + else + dat->is_checkunique = opts.checkunique; + + /* GiST indexes are supported in 1.6+ */ + dat->gist_supported = ((vmaj == 1 && vmin >= 6) || vmaj > 1); + + /* GIN indexes are supported in 1.5+ */ + dat->gin_supported = ((vmaj == 1 && vmin >= 5) || vmaj > 1); PQclear(result); @@ -651,8 +661,8 @@ main(int argc, char *argv[]) if (pat->heap_only) log_no_match("no heap tables to check matching \"%s\"", pat->pattern); - else if (pat->btree_only) - log_no_match("no btree indexes to check matching \"%s\"", + else if (pat->index_only) + log_no_match("no indexes to check matching \"%s\"", pat->pattern); else if (pat->rel_regex == NULL) log_no_match("no relations to check in schemas matching \"%s\"", @@ -785,13 +795,40 @@ main(int argc, char *argv[]) if (opts.show_progress && progress_since_last_stderr) fprintf(stderr, "\n"); - pg_log_info("checking btree index \"%s.%s.%s\"", + pg_log_info("checking index \"%s.%s.%s\"", rel->datinfo->datname, rel->nspname, rel->relname); progress_since_last_stderr = false; } - prepare_btree_command(&sql, rel, free_slot->connection); + if (rel->amoid == BTREE_AM_OID) + prepare_btree_command(&sql, rel, free_slot->connection); + else if (rel->amoid == GIST_AM_OID) + { + if (rel->datinfo->gist_supported) + prepare_gist_command(&sql, rel, free_slot->connection); + else + { + if (!gist_warn_printed) + pg_log_warning("GiST verification is not supported by installed amcheck version"); + gist_warn_printed = true; + } + } + else if (rel->amoid == GIN_AM_OID) + { + if (rel->datinfo->gin_supported) + prepare_gin_command(&sql, rel, free_slot->connection); + else + { + if (!gin_warn_printed) + pg_log_warning("GIN verification is not supported by installed amcheck version"); + gin_warn_printed = true; + } + } + else + /* should not happen at this stage */ + pg_log_info("Verification of index type %u not supported", + rel->amoid); rel->sql = pstrdup(sql.data); /* pg_free'd after command */ - ParallelSlotSetHandler(free_slot, verify_btree_slot_handler, rel); + ParallelSlotSetHandler(free_slot, verify_index_slot_handler, rel); run_command(free_slot, rel->sql); } } @@ -869,7 +906,7 @@ prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) * Creates a SQL command for running amcheck checking on the given btree index * relation. The command does not select any columns, as btree checking * functions do not return any, but rather return corruption information by - * raising errors, which verify_btree_slot_handler expects. + * raising errors, which verify_index_slot_handler expects. * * The constructed SQL command will silently skip temporary indexes, and * indexes being reindexed concurrently, as checking them would needlessly draw @@ -915,6 +952,49 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) rel->reloid); } +/* + * prepare_gist_command + * Similar to btree equivalent prepares command to check GiST index. + */ +static void +prepare_gist_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) +{ + resetPQExpBuffer(sql); + + appendPQExpBuffer(sql, + "SELECT %s.gist_index_check(" + "index := c.oid, heapallindexed := %s)" + "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i " + "WHERE c.oid = %u " + "AND c.oid = i.indexrelid " + "AND c.relpersistence != 't' " + "AND i.indisready AND i.indisvalid AND i.indislive", + rel->datinfo->amcheck_schema, + (opts.heapallindexed ? "true" : "false"), + rel->reloid); +} + +/* + * prepare_gin_command + * Similar to btree equivalent prepares command to check GIN index. + */ +static void +prepare_gin_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) +{ + resetPQExpBuffer(sql); + + appendPQExpBuffer(sql, + "SELECT %s.gin_index_check(" + "index := c.oid)" + "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i " + "WHERE c.oid = %u " + "AND c.oid = i.indexrelid " + "AND c.relpersistence != 't' " + "AND i.indisready AND i.indisvalid AND i.indislive", + rel->datinfo->amcheck_schema, + rel->reloid); +} + /* * run_command * @@ -954,7 +1034,7 @@ run_command(ParallelSlot *slot, const char *sql) * Note: Heap relation corruption is reported by verify_heapam() via the result * set, rather than an ERROR, but running verify_heapam() on a corrupted heap * table may still result in an error being returned from the server due to - * missing relation files, bad checksums, etc. The btree corruption checking + * missing relation files, bad checksums, etc. The corruption checking * functions always use errors to communicate corruption messages. We can't * just abort processing because we got a mere ERROR. * @@ -1104,11 +1184,11 @@ verify_heap_slot_handler(PGresult *res, PGconn *conn, void *context) } /* - * verify_btree_slot_handler + * verify_index_slot_handler * - * ParallelSlotHandler that receives results from a btree checking command - * created by prepare_btree_command and outputs them for the user. The results - * from the btree checking command is assumed to be empty, but when the results + * ParallelSlotHandler that receives results from a checking command created by + * prepare_[btree,gist]_command and outputs them for the user. The results + * from the checking command is assumed to be empty, but when the results * are an error code, the useful information about the corruption is expected * in the connection's error message. * @@ -1117,7 +1197,7 @@ verify_heap_slot_handler(PGresult *res, PGconn *conn, void *context) * context: unused */ static bool -verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context) +verify_index_slot_handler(PGresult *res, PGconn *conn, void *context) { RelationInfo *rel = (RelationInfo *) context; @@ -1128,12 +1208,12 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context) 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. + * We expect the 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. * * In conjunction with --progress, anything written to stderr at * this time would present strangely to the user without an extra @@ -1143,7 +1223,7 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context) */ if (opts.show_progress && progress_since_last_stderr) fprintf(stderr, "\n"); - pg_log_warning("btree index \"%s.%s.%s\": btree checking function returned unexpected number of rows: %d", + pg_log_warning("index \"%s.%s.%s\": checking function returned unexpected number of rows: %d", rel->datinfo->datname, rel->nspname, rel->relname, ntups); if (opts.verbose) pg_log_warning_detail("Query was: %s", rel->sql); @@ -1157,7 +1237,7 @@ verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context) char *msg = indent_lines(PQerrorMessage(conn)); all_checks_pass = false; - printf(_("btree index \"%s.%s.%s\":\n"), + printf(_("index \"%s.%s.%s\":\n"), rel->datinfo->datname, rel->nspname, rel->relname); printf("%s", msg); if (opts.verbose) @@ -1211,6 +1291,8 @@ 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(_("\nGiST index checking options:\n")); + printf(_(" --heapallindexed check that all heap tuples are found within indexes\n")); printf(_("\nConnection options:\n")); printf(_(" -h, --host=HOSTNAME database server host or socket directory\n")); printf(_(" -p, --port=PORT database server port\n")); @@ -1424,11 +1506,11 @@ append_schema_pattern(PatternInfoArray *pia, const char *pattern, int encoding) * pattern: the relation name pattern * encoding: client encoding for parsing the pattern * heap_only: whether the pattern should only be matched against heap tables - * btree_only: whether the pattern should only be matched against btree indexes + * index_only: whether the pattern should only be matched against indexes */ static void append_relation_pattern_helper(PatternInfoArray *pia, const char *pattern, - int encoding, bool heap_only, bool btree_only) + int encoding, bool heap_only, bool index_only) { PQExpBufferData dbbuf; PQExpBufferData nspbuf; @@ -1463,14 +1545,14 @@ append_relation_pattern_helper(PatternInfoArray *pia, const char *pattern, termPQExpBuffer(&relbuf); info->heap_only = heap_only; - info->btree_only = btree_only; + info->index_only = index_only; } /* * append_relation_pattern * * Adds the given pattern interpreted as a relation pattern, to be matched - * against both heap tables and btree indexes. + * against both heap tables and indexes. * * pia: the pattern info array to be appended * pattern: the relation name pattern @@ -1499,17 +1581,17 @@ append_heap_pattern(PatternInfoArray *pia, const char *pattern, int encoding) } /* - * append_btree_pattern + * append_index_pattern * * Adds the given pattern interpreted as a relation pattern, to be matched only - * against btree indexes. + * against indexes. * * pia: the pattern info array to be appended * pattern: the relation name pattern * encoding: client encoding for parsing the pattern */ static void -append_btree_pattern(PatternInfoArray *pia, const char *pattern, int encoding) +append_index_pattern(PatternInfoArray *pia, const char *pattern, int encoding) { append_relation_pattern_helper(pia, pattern, encoding, false, true); } @@ -1767,7 +1849,7 @@ compile_database_list(PGconn *conn, SimplePtrList *databases, * rel_regex: the relname regexp parsed from the pattern, or NULL if the * pattern had no relname part * heap_only: true if the pattern applies only to heap tables (not indexes) - * btree_only: true if the pattern applies only to btree indexes (not tables) + * index_only: true if the pattern applies only to indexes (not tables) * * buf: the buffer to be appended * patterns: the array of patterns to be inserted into the CTE @@ -1809,7 +1891,7 @@ append_rel_pattern_raw_cte(PQExpBuffer buf, const PatternInfoArray *pia, appendPQExpBufferStr(buf, "::TEXT, true::BOOLEAN"); else appendPQExpBufferStr(buf, "::TEXT, false::BOOLEAN"); - if (info->btree_only) + if (info->index_only) appendPQExpBufferStr(buf, ", true::BOOLEAN"); else appendPQExpBufferStr(buf, ", false::BOOLEAN"); @@ -1847,8 +1929,8 @@ append_rel_pattern_filtered_cte(PQExpBuffer buf, const char *raw, const char *filtered, PGconn *conn) { appendPQExpBuffer(buf, - "\n%s (pattern_id, nsp_regex, rel_regex, heap_only, btree_only) AS (" - "\nSELECT pattern_id, nsp_regex, rel_regex, heap_only, btree_only " + "\n%s (pattern_id, nsp_regex, rel_regex, heap_only, index_only) AS (" + "\nSELECT pattern_id, nsp_regex, rel_regex, heap_only, index_only " "FROM %s r" "\nWHERE (r.db_regex IS NULL " "OR ", @@ -1871,7 +1953,7 @@ append_rel_pattern_filtered_cte(PQExpBuffer buf, const char *raw, * The cells of the constructed list contain all information about the relation * necessary to connect to the database and check the object, including which * database to connect to, where contrib/amcheck is installed, and the Oid and - * type of object (heap table vs. btree index). Rather than duplicating the + * type of object (heap table vs. index). Rather than duplicating the * database details per relation, the relation structs use references to the * same database object, provided by the caller. * @@ -1898,7 +1980,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, if (!opts.allrel) { appendPQExpBufferStr(&sql, - " include_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, btree_only) AS ("); + " include_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, index_only) AS ("); append_rel_pattern_raw_cte(&sql, &opts.include, conn); appendPQExpBufferStr(&sql, "\n),"); append_rel_pattern_filtered_cte(&sql, "include_raw", "include_pat", conn); @@ -1908,7 +1990,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, if (opts.excludetbl || opts.excludeidx || opts.excludensp) { appendPQExpBufferStr(&sql, - " exclude_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, btree_only) AS ("); + " exclude_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, index_only) AS ("); append_rel_pattern_raw_cte(&sql, &opts.exclude, conn); appendPQExpBufferStr(&sql, "\n),"); append_rel_pattern_filtered_cte(&sql, "exclude_raw", "exclude_pat", conn); @@ -1916,36 +1998,36 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, /* Append the relation CTE. */ appendPQExpBufferStr(&sql, - " relation (pattern_id, oid, nspname, relname, reltoastrelid, relpages, is_heap, is_btree) AS (" + " relation (pattern_id, oid, amoid, nspname, relname, reltoastrelid, relpages, is_heap, is_index) AS (" "\nSELECT DISTINCT ON (c.oid"); if (!opts.allrel) appendPQExpBufferStr(&sql, ", ip.pattern_id) ip.pattern_id,"); else appendPQExpBufferStr(&sql, ") NULL::INTEGER AS pattern_id,"); appendPQExpBuffer(&sql, - "\nc.oid, n.nspname, c.relname, c.reltoastrelid, c.relpages, " - "c.relam = %u AS is_heap, " - "c.relam = %u AS is_btree" + "\nc.oid, c.relam as amoid, n.nspname, c.relname, " + "c.reltoastrelid, c.relpages, c.relam = %u AS is_heap, " + "(c.relam = %u OR c.relam = %u OR c.relam = %u) AS is_index" "\nFROM pg_catalog.pg_class c " "INNER JOIN pg_catalog.pg_namespace n " "ON c.relnamespace = n.oid", - HEAP_TABLE_AM_OID, BTREE_AM_OID); + HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID); if (!opts.allrel) appendPQExpBuffer(&sql, "\nINNER JOIN include_pat ip" "\nON (n.nspname ~ ip.nsp_regex OR ip.nsp_regex IS NULL)" "\nAND (c.relname ~ ip.rel_regex OR ip.rel_regex IS NULL)" "\nAND (c.relam = %u OR NOT ip.heap_only)" - "\nAND (c.relam = %u OR NOT ip.btree_only)", - HEAP_TABLE_AM_OID, BTREE_AM_OID); + "\nAND ((c.relam = %u OR c.relam = %u OR c.relam = %u) OR NOT ip.index_only)", + HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID); if (opts.excludetbl || opts.excludeidx || opts.excludensp) appendPQExpBuffer(&sql, "\nLEFT OUTER JOIN exclude_pat ep" "\nON (n.nspname ~ ep.nsp_regex OR ep.nsp_regex IS NULL)" "\nAND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL)" "\nAND (c.relam = %u OR NOT ep.heap_only OR ep.rel_regex IS NULL)" - "\nAND (c.relam = %u OR NOT ep.btree_only OR ep.rel_regex IS NULL)", - HEAP_TABLE_AM_OID, BTREE_AM_OID); + "\nAND ((c.relam = %u OR c.relam = %u OR c.relam = %u) OR NOT ep.index_only OR ep.rel_regex IS NULL)", + HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID); /* * Exclude temporary tables and indexes, which must necessarily belong to @@ -1984,7 +2066,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, HEAP_TABLE_AM_OID, PG_TOAST_NAMESPACE); else appendPQExpBuffer(&sql, - " AND c.relam IN (%u, %u)" + " AND c.relam IN (%u, %u, %u, %u)" "AND c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " CppAsString2(RELKIND_SEQUENCE) ", " @@ -1996,10 +2078,10 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, CppAsString2(RELKIND_SEQUENCE) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_TOASTVALUE) ")) OR " - "(c.relam = %u AND c.relkind = " + "((c.relam = %u OR c.relam = %u OR c.relam = %u) AND c.relkind = " CppAsString2(RELKIND_INDEX) "))", - HEAP_TABLE_AM_OID, BTREE_AM_OID, - HEAP_TABLE_AM_OID, BTREE_AM_OID); + HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID, + HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID); appendPQExpBufferStr(&sql, "\nORDER BY c.oid)"); @@ -2028,7 +2110,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, appendPQExpBufferStr(&sql, "\n)"); } - if (!opts.no_btree_expansion) + if (!opts.no_index_expansion) { /* * Include a CTE for btree indexes associated with primary heap tables @@ -2036,9 +2118,9 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, * btree index names. */ appendPQExpBufferStr(&sql, - ", index (oid, nspname, relname, relpages) AS (" - "\nSELECT c.oid, r.nspname, c.relname, c.relpages " - "FROM relation r" + ", index (oid, amoid, nspname, relname, relpages) AS (" + "\nSELECT c.oid, c.relam as amoid, r.nspname, " + "c.relname, c.relpages FROM relation r" "\nINNER JOIN pg_catalog.pg_index i " "ON r.oid = i.indrelid " "INNER JOIN pg_catalog.pg_class c " @@ -2051,15 +2133,15 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, "\nLEFT OUTER JOIN exclude_pat ep " "ON (n.nspname ~ ep.nsp_regex OR ep.nsp_regex IS NULL) " "AND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL) " - "AND ep.btree_only" + "AND ep.index_only" "\nWHERE ep.pattern_id IS NULL"); else appendPQExpBufferStr(&sql, "\nWHERE true"); appendPQExpBuffer(&sql, - " AND c.relam = %u " + " AND (c.relam = %u or c.relam = %u or c.relam = %u) " "AND c.relkind = " CppAsString2(RELKIND_INDEX), - BTREE_AM_OID); + BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID); if (opts.no_toast_expansion) appendPQExpBuffer(&sql, " AND c.relnamespace != %u", @@ -2067,7 +2149,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, appendPQExpBufferStr(&sql, "\n)"); } - if (!opts.no_toast_expansion && !opts.no_btree_expansion) + if (!opts.no_toast_expansion && !opts.no_index_expansion) { /* * Include a CTE for btree indexes associated with toast tables of @@ -2088,7 +2170,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, "\nLEFT OUTER JOIN exclude_pat ep " "ON ('pg_toast' ~ ep.nsp_regex OR ep.nsp_regex IS NULL) " "AND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL) " - "AND ep.btree_only " + "AND ep.index_only " "WHERE ep.pattern_id IS NULL"); else appendPQExpBufferStr(&sql, @@ -2108,12 +2190,13 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, * list. */ appendPQExpBufferStr(&sql, - "\nSELECT pattern_id, is_heap, is_btree, oid, nspname, relname, relpages " + "\nSELECT pattern_id, is_heap, is_index, oid, amoid, nspname, relname, relpages " "FROM ("); appendPQExpBufferStr(&sql, /* Inclusion patterns that failed to match */ - "\nSELECT pattern_id, is_heap, is_btree, " + "\nSELECT pattern_id, is_heap, is_index, " "NULL::OID AS oid, " + "NULL::OID AS amoid, " "NULL::TEXT AS nspname, " "NULL::TEXT AS relname, " "NULL::INTEGER AS relpages" @@ -2122,29 +2205,29 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, "UNION" /* Primary relations */ "\nSELECT NULL::INTEGER AS pattern_id, " - "is_heap, is_btree, oid, nspname, relname, relpages " + "is_heap, is_index, oid, amoid, nspname, relname, relpages " "FROM relation"); if (!opts.no_toast_expansion) - appendPQExpBufferStr(&sql, - " UNION" + appendPQExpBuffer(&sql, + " UNION" /* Toast tables for primary relations */ - "\nSELECT NULL::INTEGER AS pattern_id, TRUE AS is_heap, " - "FALSE AS is_btree, oid, nspname, relname, relpages " - "FROM toast"); - if (!opts.no_btree_expansion) + "\nSELECT NULL::INTEGER AS pattern_id, TRUE AS is_heap, " + "FALSE AS is_index, oid, 0 as amoid, nspname, relname, relpages " + "FROM toast"); + if (!opts.no_index_expansion) appendPQExpBufferStr(&sql, " UNION" /* Indexes for primary relations */ "\nSELECT NULL::INTEGER AS pattern_id, FALSE AS is_heap, " - "TRUE AS is_btree, oid, nspname, relname, relpages " + "TRUE AS is_index, oid, amoid, nspname, relname, relpages " "FROM index"); - if (!opts.no_toast_expansion && !opts.no_btree_expansion) - appendPQExpBufferStr(&sql, - " UNION" + if (!opts.no_toast_expansion && !opts.no_index_expansion) + appendPQExpBuffer(&sql, + " UNION" /* Indexes for toast relations */ - "\nSELECT NULL::INTEGER AS pattern_id, FALSE AS is_heap, " - "TRUE AS is_btree, oid, nspname, relname, relpages " - "FROM toast_index"); + "\nSELECT NULL::INTEGER AS pattern_id, FALSE AS is_heap, " + "TRUE AS is_index, oid, %u as amoid, nspname, relname, relpages " + "FROM toast_index", BTREE_AM_OID); appendPQExpBufferStr(&sql, "\n) AS combined_records " "ORDER BY relpages DESC NULLS FIRST, oid"); @@ -2164,8 +2247,9 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, { int pattern_id = -1; bool is_heap = false; - bool is_btree PG_USED_FOR_ASSERTS_ONLY = false; + bool is_index PG_USED_FOR_ASSERTS_ONLY = false; Oid oid = InvalidOid; + Oid amoid = InvalidOid; const char *nspname = NULL; const char *relname = NULL; int relpages = 0; @@ -2175,15 +2259,17 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, if (!PQgetisnull(res, i, 1)) is_heap = (PQgetvalue(res, i, 1)[0] == 't'); if (!PQgetisnull(res, i, 2)) - is_btree = (PQgetvalue(res, i, 2)[0] == 't'); + is_index = (PQgetvalue(res, i, 2)[0] == 't'); if (!PQgetisnull(res, i, 3)) oid = atooid(PQgetvalue(res, i, 3)); if (!PQgetisnull(res, i, 4)) - nspname = PQgetvalue(res, i, 4); + amoid = atooid(PQgetvalue(res, i, 4)); if (!PQgetisnull(res, i, 5)) - relname = PQgetvalue(res, i, 5); + nspname = PQgetvalue(res, i, 5); if (!PQgetisnull(res, i, 6)) - relpages = atoi(PQgetvalue(res, i, 6)); + relname = PQgetvalue(res, i, 6); + if (!PQgetisnull(res, i, 7)) + relpages = atoi(PQgetvalue(res, i, 7)); if (pattern_id >= 0) { @@ -2205,10 +2291,11 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, RelationInfo *rel = (RelationInfo *) pg_malloc0(sizeof(RelationInfo)); Assert(OidIsValid(oid)); - Assert((is_heap && !is_btree) || (is_btree && !is_heap)); + Assert((is_heap && !is_index) || (is_index && !is_heap)); rel->datinfo = dat; rel->reloid = oid; + rel->amoid = amoid; rel->is_heap = is_heap; rel->nspname = pstrdup(nspname); rel->relname = pstrdup(relname); @@ -2218,7 +2305,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, { /* * We apply --startblock and --endblock to heap tables, but - * not btree indexes, and for progress purposes we need to + * not supported indexes, and for progress purposes we need to * track how many blocks we expect to check. */ if (opts.endblock >= 0 && rel->blocks_to_check > opts.endblock) diff --git a/src/bin/pg_amcheck/t/002_nonesuch.pl b/src/bin/pg_amcheck/t/002_nonesuch.pl index f23368abeab3..e11cc4e61583 100644 --- a/src/bin/pg_amcheck/t/002_nonesuch.pl +++ b/src/bin/pg_amcheck/t/002_nonesuch.pl @@ -285,8 +285,8 @@ [ qr/pg_amcheck: warning: no heap tables to check matching "no_such_table"/, qr/pg_amcheck: warning: no heap tables to check matching "no\*such\*table"/, - qr/pg_amcheck: warning: no btree indexes to check matching "no_such_index"/, - qr/pg_amcheck: warning: no btree indexes to check matching "no\*such\*index"/, + qr/pg_amcheck: warning: no indexes to check matching "no_such_index"/, + qr/pg_amcheck: warning: no indexes to check matching "no\*such\*index"/, qr/pg_amcheck: warning: no relations to check matching "no_such_relation"/, qr/pg_amcheck: warning: no relations to check matching "no\*such\*relation"/, qr/pg_amcheck: warning: no heap tables to check matching "no\*such\*table"/, @@ -366,8 +366,8 @@ qr/pg_amcheck: warning: no heap tables to check matching "template1\.public\.foo"/, qr/pg_amcheck: warning: no heap tables to check matching "another_db\.public\.foo"/, qr/pg_amcheck: warning: no connectable databases to check matching "no_such_database\.public\.foo"/, - qr/pg_amcheck: warning: no btree indexes to check matching "template1\.public\.foo_idx"/, - qr/pg_amcheck: warning: no btree indexes to check matching "another_db\.public\.foo_idx"/, + qr/pg_amcheck: warning: no indexes to check matching "template1\.public\.foo_idx"/, + qr/pg_amcheck: warning: no indexes to check matching "another_db\.public\.foo_idx"/, qr/pg_amcheck: warning: no connectable databases to check matching "no_such_database\.public\.foo_idx"/, qr/pg_amcheck: error: no relations to check/, ], diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl index 881854da254b..f22146d14663 100644 --- a/src/bin/pg_amcheck/t/003_check.pl +++ b/src/bin/pg_amcheck/t/003_check.pl @@ -1,4 +1,3 @@ - # Copyright (c) 2021-2025, PostgreSQL Global Development Group use strict; @@ -185,7 +184,7 @@ () # schemas. The schemas are all identical to start, but # we will corrupt them differently later. # - for my $schema (qw(s1 s2 s3 s4 s5)) + for my $schema (qw(s1 s2 s3 s4 s5 s6 s7)) { $node->safe_psql( $dbname, qq( @@ -291,22 +290,26 @@ () # Corrupt toast table, partitions, and materialized views in schema "s4" plan_to_remove_toast_file('db1', 's4.t2'); -# Corrupt all other object types in schema "s5". We don't have amcheck support +# Corrupt GiST index in schema "s5" +plan_to_remove_relation_file('db1', 's5.t1_gist'); +plan_to_corrupt_first_page('db1', 's5.t2_gist'); + +# Corrupt GIN index in schema "s6" +plan_to_remove_relation_file('db1', 's6.t1_gin'); +plan_to_corrupt_first_page('db1', 's6.t2_gin'); + +# Corrupt all other object types in schema "s7". We don't have amcheck support # for these types, but we check that their corruption does not trigger any # errors in pg_amcheck -plan_to_remove_relation_file('db1', 's5.seq1'); -plan_to_remove_relation_file('db1', 's5.t1_hash'); -plan_to_remove_relation_file('db1', 's5.t1_gist'); -plan_to_remove_relation_file('db1', 's5.t1_gin'); -plan_to_remove_relation_file('db1', 's5.t1_brin'); -plan_to_remove_relation_file('db1', 's5.t1_spgist'); +plan_to_remove_relation_file('db1', 's7.seq1'); +plan_to_remove_relation_file('db1', 's7.t1_hash'); +plan_to_remove_relation_file('db1', 's7.t1_brin'); +plan_to_remove_relation_file('db1', 's7.t1_spgist'); -plan_to_corrupt_first_page('db1', 's5.seq2'); -plan_to_corrupt_first_page('db1', 's5.t2_hash'); -plan_to_corrupt_first_page('db1', 's5.t2_gist'); -plan_to_corrupt_first_page('db1', 's5.t2_gin'); -plan_to_corrupt_first_page('db1', 's5.t2_brin'); -plan_to_corrupt_first_page('db1', 's5.t2_spgist'); +plan_to_corrupt_first_page('db1', 's7.seq2'); +plan_to_corrupt_first_page('db1', 's7.t2_hash'); +plan_to_corrupt_first_page('db1', 's7.t2_brin'); +plan_to_corrupt_first_page('db1', 's7.t2_spgist'); # Database 'db2' corruptions @@ -461,10 +464,34 @@ () [$no_output_re], 'pg_amcheck in schema s4 excluding toast reports no corruption'); -# Check that no corruption is reported in schema db1.s5 -$node->command_checks_all([ @cmd, '--schema' => 's5', 'db1' ], +# In schema db1.s5 we should see GiST corruption messages on stdout, and +# nothing on stderr. +# +$node->command_checks_all( + [ @cmd, '-s', 's5', 'db1' ], + 2, + [ + $missing_file_re, $line_pointer_corruption_re, + ], + [$no_output_re], + 'pg_amcheck schema s5 reports GiST index errors'); + +# In schema db1.s6 we should see GIN corruption messages on stdout, and +# nothing on stderr. +# +$node->command_checks_all( + [ @cmd, '-s', 's6', 'db1' ], + 2, + [ + $missing_file_re, + ], + [$no_output_re], + 'pg_amcheck schema s6 reports GIN index errors'); + +# Check that no corruption is reported in schema db1.s7 +$node->command_checks_all([ @cmd, '-s', 's7', 'db1' ], 0, [$no_output_re], [$no_output_re], - 'pg_amcheck over schema s5 reports no corruption'); + 'pg_amcheck over schema s7 reports no corruption'); # In schema db1.s1, only indexes are corrupt. Verify that when we exclude # the indexes, no corruption is reported about the schema. @@ -619,7 +646,7 @@ () 'pg_amcheck excluding all corrupt schemas with --checkunique option'); # -# Smoke test for checkunique option for not supported versions. +# Smoke test for checkunique option and GiST indexes for not supported versions. # $node->safe_psql( 'db3', q( @@ -635,4 +662,28 @@ () qr/pg_amcheck: warning: option --checkunique is not supported by amcheck version 1.3/ ], 'pg_amcheck smoke test --checkunique'); + +$node->safe_psql( + 'db1', q( + DROP EXTENSION amcheck; + CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema VERSION '1.3' ; +)); + +$node->command_checks_all( + [ @cmd, '-s', 's5', 'db1' ], + 0, + [$no_output_re], + [ + qr/pg_amcheck: warning: GiST verification is not supported by installed amcheck version/ + ], + 'pg_amcheck smoke test GiST version warning'); + +$node->command_checks_all( + [ @cmd, '-s', 's6', 'db1' ], + 0, + [$no_output_re], + [ + qr/pg_amcheck: warning: GIN verification is not supported by installed amcheck version/ + ], + 'pg_amcheck smoke test GIN version warning'); done_testing();