From 89660718827121c302ca35543babcafc7c92daf4 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Wed, 2 Jul 2025 21:21:26 +0300
Subject: [PATCH v2025-07-11 3/3] GIN in pg_amcheck

---
 src/bin/pg_amcheck/pg_amcheck.c   | 63 +++++++++++++++++++++++++------
 src/bin/pg_amcheck/t/003_check.pl | 56 ++++++++++++++++++---------
 2 files changed, 90 insertions(+), 29 deletions(-)

diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 2575178cd1a..272d0fde708 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -151,6 +151,7 @@ typedef struct DatabaseInfo
 	char	   *amcheck_schema; /* escaped, quoted literal */
 	bool		is_checkunique;
 	bool		gist_supported;
+	bool		gin_supported;
 } DatabaseInfo;
 
 typedef struct RelationInfo
@@ -181,6 +182,8 @@ 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);
@@ -291,6 +294,7 @@ main(int argc, char *argv[])
 	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]);
@@ -634,6 +638,9 @@ main(int argc, char *argv[])
 		/* 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);
 
 		compile_relation_list_one_db(conn, &relations, dat, &pagestotal);
@@ -805,6 +812,17 @@ main(int argc, char *argv[])
 					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",
@@ -956,6 +974,27 @@ prepare_gist_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
 					  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
  *
@@ -1968,27 +2007,27 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 	appendPQExpBuffer(&sql,
 					  "\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) AS is_index"
+					  "(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, GIST_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 c.relam = %u) OR NOT ip.index_only)",
-						  HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_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 c.relam = %u) OR NOT ep.index_only OR ep.rel_regex IS NULL)",
-						  HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_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
@@ -2027,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, %u)"
+						  " AND c.relam IN (%u, %u, %u, %u)"
 						  "AND c.relkind IN ("
 						  CppAsString2(RELKIND_RELATION) ", "
 						  CppAsString2(RELKIND_SEQUENCE) ", "
@@ -2039,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 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, GIST_AM_OID,
-						  HEAP_TABLE_AM_OID, BTREE_AM_OID, GIST_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)");
@@ -2100,9 +2139,9 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
 			appendPQExpBufferStr(&sql,
 								 "\nWHERE true");
 		appendPQExpBuffer(&sql,
-						  " AND (c.relam = %u or c.relam = %u) "
+						  " AND (c.relam = %u or c.relam = %u or c.relam = %u) "
 						  "AND c.relkind = " CppAsString2(RELKIND_INDEX),
-						  BTREE_AM_OID, GIST_AM_OID);
+						  BTREE_AM_OID, GIST_AM_OID, GIN_AM_OID);
 		if (opts.no_toast_expansion)
 			appendPQExpBuffer(&sql,
 							  " AND c.relnamespace != %u",
diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl
index 2d6efbf8b05..f22146d1466 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 @@ for my $dbname (qw(db1 db2 db3))
 	# schemas.  The schemas are all identical to start, but
 	# we will corrupt them differently later.
 	#
-	for my $schema (qw(s1 s2 s3 s4 s5 s6))
+	for my $schema (qw(s1 s2 s3 s4 s5 s6 s7))
 	{
 		$node->safe_psql(
 			$dbname, qq(
@@ -295,20 +294,22 @@ plan_to_remove_toast_file('db1', 's4.t2');
 plan_to_remove_relation_file('db1', 's5.t1_gist');
 plan_to_corrupt_first_page('db1', 's5.t2_gist');
 
-# Corrupt all other object types in schema "s6".  We don't have amcheck support
+# 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', 's6.seq1');
-plan_to_remove_relation_file('db1', 's6.t1_hash');
-plan_to_remove_relation_file('db1', 's6.t1_gin');
-plan_to_remove_relation_file('db1', 's6.t1_brin');
-plan_to_remove_relation_file('db1', 's6.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', 's6.seq2');
-plan_to_corrupt_first_page('db1', 's6.t2_hash');
-plan_to_corrupt_first_page('db1', 's6.t2_gin');
-plan_to_corrupt_first_page('db1', 's6.t2_brin');
-plan_to_corrupt_first_page('db1', 's6.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
@@ -475,10 +476,22 @@ $node->command_checks_all(
 	[$no_output_re],
 	'pg_amcheck schema s5 reports GiST index errors');
 
-# Check that no corruption is reported in schema db1.s6
-$node->command_checks_all([ @cmd, '-s', 's6', 'db1' ],
+# 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 s6 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.
@@ -663,5 +676,14 @@ $node->command_checks_all(
 	[
 		qr/pg_amcheck: warning: GiST verification is not supported by installed amcheck version/
 	],
-	'pg_amcheck smoke test --checkunique');
+	'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();
-- 
2.39.5 (Apple Git-154)

