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();