From 1f35767b3c9e4ef682e472050012e931731e095e Mon Sep 17 00:00:00 2001
From: roman khapov <r.khapov@ya.ru>
Date: Fri, 3 Jul 2026 15:54:42 +0500
Subject: [PATCH v3] introduce pg_drop_invalid_indexes

This patch adds new builtin function
pg_drop_invalid_indexes, which helps with removal of invalid
indexes that can appear after failed CREATE INDEX CONCURRENTLY
operations or etc. Previously required manual queries against
pg_index, which was error-prone.

Example of usage:
postgres=# \d+ sas
                                           Table "public.sas"
 Column |  Type   | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
 id     | integer |           |          |         | plain    |             |              |
 t      | text    |           |          |         | extended |             |              |
Indexes:
    "idx1" btree (id) INVALID
Access method: heap

postgres=# select * from pg_drop_invalid_indexes('sas');
-[ RECORD 1 ]-----
index_name | idx1
index_oid  | 16842

postgres=# \d+ sas
                                           Table "public.sas"
 Column |  Type   | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
 id     | integer |           |          |         | plain    |             |              |
 t      | text    |           |          |         | extended |             |              |
Access method: heap

Co-authored-by: Kirill Reshke <reshkekirill@gmail.com>
Signed-off-by: Roman Khapov <r.khapov@ya.ru>
---
 src/backend/catalog/index.c                   |  6 ++
 src/backend/commands/indexcmds.c              | 78 +++++++++++++++++++
 src/include/catalog/pg_proc.dat               | 13 ++++
 .../test_misc/t/015_drop_invalid_indexes.pl   | 75 ++++++++++++++++++
 4 files changed, 172 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/015_drop_invalid_indexes.pl

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..69976b21010 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -73,6 +73,7 @@
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -3100,6 +3101,11 @@ index_build(Relation heapRelation,
 											 indexInfo);
 	Assert(stats);
 
+
+#ifdef USE_INJECTION_POINTS
+	INJECTION_POINT("index-build-after-am-callback", NULL);
+#endif
+
 	/*
 	 * If this is an unlogged index, we may need to write out an init fork for
 	 * it -- but we must first check whether one already exists.  If, for
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 713bb5d10f1..448fa2e7d4e 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -20,11 +20,13 @@
 #include "access/gist.h"
 #include "access/heapam.h"
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "access/reloptions.h"
 #include "access/sysattr.h"
 #include "access/tableam.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "catalog/dependency.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
@@ -44,6 +46,7 @@
 #include "commands/progress.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
+#include "funcapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -71,6 +74,7 @@
 #include "utils/regproc.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "utils/tuplestore.h"
 
 
 /* context for ChooseIndexExpressionName_walker */
@@ -4782,3 +4786,77 @@ set_indexsafe_procflags(void)
 	ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;
 	LWLockRelease(ProcArrayLock);
 }
+
+/*
+ * Drops invalid indexes from given table
+ */
+Datum
+pg_drop_invalid_indexes(PG_FUNCTION_ARGS)
+{
+	Oid			relOid = PG_GETARG_OID(0);
+	bool		skipLocked = PG_GETARG_BOOL(1);
+	bool		cascade = PG_GETARG_BOOL(2);
+	ObjectAddresses *indexesToDrop;
+	List	   *indexesList;
+	ListCell   *cell;
+	Relation	relation;
+	ObjectAddress obj;
+	LOCKMODE	lockmode = ShareUpdateExclusiveLock;
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	int			flags = 0;
+
+	(void) skipLocked;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	indexesToDrop = new_object_addresses();
+
+	relation = table_open(relOid, lockmode);
+	indexesList = RelationGetIndexList(relation);
+
+	foreach(cell, indexesList)
+	{
+		Oid			indexoid = lfirst_oid(cell);
+		Relation	indrel;
+		bool		isValid;
+		char	   *idxname;
+
+		indrel = index_open(indexoid, AccessExclusiveLock);
+
+		isValid = indrel->rd_index->indisvalid;
+		idxname = isValid ? NULL : pstrdup(indrel->rd_rel->relname.data);
+
+		index_close(indrel, AccessExclusiveLock);
+
+		if (!isValid)
+		{
+			Datum		values[2];
+			bool		nulls[2] = {false, false};
+
+			obj.classId = RelationRelationId;
+			obj.objectId = indexoid;
+			obj.objectSubId = 0;
+
+			add_exact_object_address(&obj, indexesToDrop);
+
+			values[0] = CStringGetTextDatum(idxname);
+			values[1] = ObjectIdGetDatum(indexoid);
+
+			tuplestore_putvalues(rsinfo->setResult,
+								 rsinfo->setDesc,
+								 values, nulls);
+
+			pfree(idxname);
+		}
+	}
+
+	table_close(relation, lockmode);
+
+	list_free(indexesList);
+
+	performMultipleDeletions(indexesToDrop, cascade ? DROP_CASCADE : DROP_RESTRICT, flags);
+
+	free_object_addresses(indexesToDrop);
+
+	return (Datum) 0;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a0891461411 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12343,6 +12343,19 @@
   prorettype => 'bool', proargtypes => 'regclass',
   prosrc => 'pg_relation_is_publishable' },
 
+{ oid => '8383',
+  descr => 'drop invalid indexes from an object',
+  proname => 'pg_drop_invalid_indexes',
+  prorows => '10', proretset => 't',
+  prorettype => 'record',
+  proargtypes => 'regclass bool bool',
+  proallargtypes => '{regclass,bool,bool,text,oid}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{relation,skip_locked,cascade,index_name,index_oid}',
+  pronargdefaults => '2',
+  proargdefaults => '{true,false}',
+  prosrc => 'pg_drop_invalid_indexes' },
+
 # rls
 { oid => '3298',
   descr => 'row security for current context active on table by table oid',
diff --git a/src/test/modules/test_misc/t/015_drop_invalid_indexes.pl b/src/test/modules/test_misc/t/015_drop_invalid_indexes.pl
new file mode 100644
index 00000000000..ce7ac968fae
--- /dev/null
+++ b/src/test/modules/test_misc/t/015_drop_invalid_indexes.pl
@@ -0,0 +1,75 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::BackgroundPsql;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('drop_invalid_index');
+$node->init;
+$node->start;
+
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql('postgres', q(CREATE TABLE tt (i INT PRIMARY KEY);));
+
+$node->safe_psql(
+	'postgres',
+	q{
+      INSERT INTO tt SELECT generate_series(1,10);
+    }
+);
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('index-build-after-am-callback', 'wait');");
+
+my $psql1 = $node->background_psql('postgres',  wait => 0);
+
+$psql1->query_safe(qq[SET application_name TO drop_invalid_index;]);
+
+# This pauses on the injection point while populating catcache list
+# for functions with name "foofunc"
+$psql1->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   REINDEX TABLE CONCURRENTLY tt;
+));
+
+$node->safe_psql(
+    'postgres',
+	q{
+      select pg_cancel_backend(pid) from pg_stat_activity where application_name = 'drop_invalid_index';
+    }
+);
+
+
+is( $node->safe_psql(
+		'postgres',
+		"select count(1) from pg_index where not indisvalid;"),
+	'1',
+	'dropped invalid index');
+
+$node->safe_psql(
+    'postgres',
+	q{
+		select * from pg_drop_invalid_indexes('tt');
+    }
+);
+
+is( $node->safe_psql(
+		'postgres',
+		"select count(1) from pg_index where not indisvalid;"),
+	'0',
+	'dropped invalid index');
+
+
+done_testing();
-- 
2.50.1 (Apple Git-155)

