From 8587ddec3d498afc065e58cd444b3f24acc6dbd6 Mon Sep 17 00:00:00 2001
From: roman khapov <r.khapov@ya.ru>
Date: Tue, 30 Jun 2026 15:03:55 +0500
Subject: [PATCH] Add DROP INVALID INDEXES ON TABLE command

Automates 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.

Uses ShareUpdateExclusiveLock to minimize impact on concurrent
operations. Checks indisvalid and indisready flags to avoid
dropping indexes still being created.

Example of usage:
postgres=# drop invalid indexes on table sas;
NOTICE:  dropping index "idx2"
NOTICE:  dropping index "idx5"
DROP INVALID INDEXES
postgres=#

Currently without CONCURRENCY mode support.

Co-authored-by: Kirill Reshke <reshkekirill@gmail.com>
Signed-off-by: roman khapov <r.khapov@ya.ru>
---
 src/backend/commands/tablecmds.c | 81 ++++++++++++++++++++++++++++++++
 src/backend/parser/gram.y        | 35 +++++++++++++-
 src/backend/tcop/utility.c       | 38 +++++++++++++++
 src/bin/psql/tab-complete.in.c   | 12 +++++
 src/include/commands/tablecmds.h |  2 +
 src/include/nodes/parsenodes.h   | 13 +++++
 src/include/parser/kwlist.h      |  1 +
 src/include/tcop/cmdtaglist.h    |  1 +
 src/tools/pgindent/typedefs.list |  1 +
 9 files changed, 182 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 472db112fa7..8f283633786 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1587,6 +1587,87 @@ DropErrorMsgWrongType(const char *relname, char wrongkind, char rightkind)
 			 (wentry->kind != '\0') ? errhint("%s", _(wentry->drophint_msg)) : 0));
 }
 
+void
+RemoveInvalidIndexes(DropInvalidIndexesStmt *drop)
+{
+	ObjectAddresses *indexesToDrop;
+	List	   *indexesList;
+	ListCell   *cell;
+	Relation	relation;
+	Oid			relOid;
+	ObjectAddress obj;
+	RangeVar   *rel;
+	LOCKMODE	lockmode = ShareUpdateExclusiveLock;
+	int			flags = 0;
+	struct DropRelationCallbackState state;
+
+	if (drop->concurrent)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("DROP INVALID INDEXES CONCURRENTLY is not supported")));
+
+	if (list_length(drop->tablenames) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("DROP INVALID INDEXES is not supported for multiple table now")));
+
+	indexesToDrop = new_object_addresses();
+
+	cell = list_head(drop->tablenames);
+
+	rel = makeRangeVarFromNameList((List *) lfirst(cell));
+	state.expected_relkind = RELKIND_RELATION;
+	/* Use ShareUpdateExclusiveLock if implementing concurrent */
+	state.heap_lockmode = AccessExclusiveLock;
+	state.heapOid = InvalidOid;
+	state.partParentOid = InvalidOid;
+
+	relOid = RangeVarGetRelidExtended(rel, lockmode, RVR_MISSING_OK,
+									  RangeVarCallbackForDropRelation,
+									  &state);
+
+	if (!OidIsValid(relOid))
+	{
+		DropErrorMsgNonExistent(rel, RELKIND_RELATION,
+								false /* missing is not ok now */ );
+		return;
+	}
+
+	relation = table_open(relOid, lockmode);
+	indexesList = RelationGetIndexList(relation);
+
+	foreach(cell, indexesList)
+	{
+		Oid			indexoid = lfirst_oid(cell);
+		Relation	indrel;
+
+		indrel = index_open(indexoid, AccessExclusiveLock);
+
+		if (!indrel->rd_index->indisvalid)
+		{
+			ereport(NOTICE,
+					(errmsg("dropping index \"%s\"",
+							RelationGetRelationName(indrel))));
+
+			obj.classId = RelationRelationId;
+			obj.objectId = indexoid;
+			obj.objectSubId = 0;
+
+			add_exact_object_address(&obj, indexesToDrop);
+		}
+
+		index_close(indrel, AccessExclusiveLock);
+	}
+
+	table_close(relation, lockmode);
+
+	list_free(indexesList);
+
+	performMultipleDeletions(indexesToDrop, drop->behavior, flags);
+
+	free_object_addresses(indexesToDrop);
+}
+
 /*
  * RemoveRelations
  *		Implements DROP TABLE, DROP INDEX, DROP SEQUENCE, DROP VIEW,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..b8d0ebd18d3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -298,7 +298,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreatePropGraphStmt AlterPropGraphStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
-		DropOpClassStmt DropOpFamilyStmt DropStmt
+		DropInvalidIndexesStmt DropOpClassStmt DropOpFamilyStmt DropStmt
 		DropCastStmt DropRoleStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
@@ -778,7 +778,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
 	INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
-	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
+	INTERSECT INTERVAL INTO INVALID_P INVOKER IS ISNULL ISOLATION
 
 	JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG
 	JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE
@@ -1118,6 +1118,7 @@ stmt:
 			| DiscardStmt
 			| DoStmt
 			| DropCastStmt
+			| DropInvalidIndexesStmt
 			| DropOpClassStmt
 			| DropOpFamilyStmt
 			| DropOwnedStmt
@@ -7100,6 +7101,35 @@ ReassignOwnedStmt:
 				}
 		;
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *
+ *		DROP INVALID INDEXES [ CONCURRENTLY ] ON TABLE tablename [, tablename ...] [ RESTRICT | CASCADE ]
+ *
+ *****************************************************************************/
+
+DropInvalidIndexesStmt:
+			DROP INVALID_P INDEXES ON TABLE any_name_list opt_drop_behavior
+				{
+					DropInvalidIndexesStmt *n = makeNode(DropInvalidIndexesStmt);
+
+					n->tablenames = $6;
+					n->behavior = $7;
+					n->concurrent = false;
+					$$ = (Node *) n;
+				}
+			| DROP INVALID_P INDEXES CONCURRENTLY ON TABLE any_name_list opt_drop_behavior
+				{
+					DropInvalidIndexesStmt *n = makeNode(DropInvalidIndexesStmt);
+
+					n->tablenames = $7;
+					n->behavior = $8;
+					n->concurrent = true;
+					$$ = (Node *) n;
+				}
+		;
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -18958,6 +18988,7 @@ unreserved_keyword:
 			| INSENSITIVE
 			| INSERT
 			| INSTEAD
+			| INVALID_P
 			| INVOKER
 			| ISOLATION
 			| KEEP
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..33cb6978b0e 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -82,6 +82,7 @@ static void ProcessUtilitySlow(ParseState *pstate,
 							   DestReceiver *dest,
 							   QueryCompletion *qc);
 static void ExecDropStmt(DropStmt *stmt, bool isTopLevel);
+static void ExecDropInvalidIndexes(DropInvalidIndexesStmt *stmt, bool isTopLevel);
 
 /*
  * CommandIsReadOnly: is an executable query read-only?
@@ -200,6 +201,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_DropOwnedStmt:
 		case T_DropRoleStmt:
 		case T_DropStmt:
+		case T_DropInvalidIndexesStmt:
 		case T_DropSubscriptionStmt:
 		case T_DropTableSpaceStmt:
 		case T_DropUserMappingStmt:
@@ -969,6 +971,19 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
+		case T_DropInvalidIndexesStmt:
+			{
+				DropInvalidIndexesStmt *stmt = (DropInvalidIndexesStmt *) parsetree;
+
+				if (EventTriggerSupportsObjectType(OBJECT_INDEX))
+					ProcessUtilitySlow(pstate, pstmt, queryString,
+									   context, params, queryEnv,
+									   dest, qc);
+				else
+					ExecDropInvalidIndexes(stmt, isTopLevel);
+			}
+			break;
+
 		case T_DropStmt:
 			{
 				DropStmt   *stmt = (DropStmt *) parsetree;
@@ -1782,6 +1797,12 @@ ProcessUtilitySlow(ParseState *pstate,
 				commandCollected = true;
 				break;
 
+			case T_DropInvalidIndexesStmt:
+				ExecDropInvalidIndexes((DropInvalidIndexesStmt *) parsetree, isTopLevel);
+				/* no commands stashed for DROP */
+				commandCollected = true;
+				break;
+
 			case T_DropStmt:
 				ExecDropStmt((DropStmt *) parsetree, isTopLevel);
 				/* no commands stashed for DROP */
@@ -2029,6 +2050,15 @@ ExecDropStmt(DropStmt *stmt, bool isTopLevel)
 	}
 }
 
+static void
+ExecDropInvalidIndexes(DropInvalidIndexesStmt *stmt, bool isTopLevel)
+{
+	if (stmt->concurrent)
+		PreventInTransactionBlock(isTopLevel,
+								  "DROP INVALID INDEXES CONCURRENTLY");
+
+	RemoveInvalidIndexes(stmt);
+}
 
 /*
  * UtilityReturnsTuples
@@ -2564,6 +2594,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_IMPORT_FOREIGN_SCHEMA;
 			break;
 
+		case T_DropInvalidIndexesStmt:
+			tag = CMDTAG_DROP_INVALID_INDEXES;
+			break;
+
 		case T_DropStmt:
 			switch (((DropStmt *) parsetree)->removeType)
 			{
@@ -3523,6 +3557,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_DropInvalidIndexesStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_DropdbStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 46b9add0604..897e9609c24 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1335,6 +1335,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"FUNCTION", NULL, NULL, Query_for_list_of_functions},
 	{"GROUP", Query_for_list_of_roles},
 	{"INDEX", NULL, NULL, &Query_for_list_of_indexes},
+	{"INVALID", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},
 	{"LANGUAGE", Query_for_list_of_languages},
 	{"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
 	{"MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews},
@@ -4430,6 +4431,17 @@ match_previous_words(int pattern_id,
 	else if (Matches("DROP", "INDEX", "CONCURRENTLY", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP INVALID INDEXES */
+	else if (Matches("DROP", "INVALID"))
+		COMPLETE_WITH("INDEXES ON TABLE");
+	else if (Matches("DROP", "INVALID", "INDEXES"))
+		COMPLETE_WITH("ON TABLE");
+	else if (Matches("DROP", "INVALID", "INDEXES", "ON"))
+		COMPLETE_WITH("TABLE");
+	else if (Matches("DROP", "INVALID", "INDEXES", "ON", "TABLE"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+
+
 	/* DROP MATERIALIZED VIEW */
 	else if (Matches("DROP", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..70dff275d4b 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -32,6 +32,8 @@ extern TupleDesc BuildDescForRelation(const List *columns);
 
 extern void RemoveRelations(DropStmt *drop);
 
+extern void RemoveInvalidIndexes(DropInvalidIndexesStmt *drop);
+
 extern Oid	AlterTableLookupRelation(AlterTableStmt *stmt, LOCKMODE lockmode);
 
 extern void AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4133c404a6b..43cbabc77af 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3444,6 +3444,19 @@ typedef struct DropStmt
 	bool		concurrent;		/* drop index concurrently? */
 } DropStmt;
 
+/* ----------------------
+ *		Drop invalid indexes on table Statement
+ * ----------------------
+ */
+
+typedef struct DropInvalidIndexesStmt
+{
+	NodeTag			type;
+	List			*tablenames;		/* list of table names */
+	DropBehavior	behavior;			/* RESTRICT or CASCADE behavior */
+	bool			concurrent;			/* drop index concurrently? */
+} DropInvalidIndexesStmt;
+
 /* ----------------------
  *				Truncate Table Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 51ead54f015..7754c7e6304 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -234,6 +234,7 @@ PG_KEYWORD("integer", INTEGER, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("intersect", INTERSECT, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("interval", INTERVAL, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("into", INTO, RESERVED_KEYWORD, AS_LABEL)
+PG_KEYWORD("invalid", INVALID_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("invoker", INVOKER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index befae5f6b4f..3080208045b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -150,6 +150,7 @@ PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, f
 PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_INVALID_INDEXES, "DROP INVALID INDEXES", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_MATERIALIZED_VIEW, "DROP MATERIALIZED VIEW", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_OPERATOR, "DROP OPERATOR", true, false, false)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..d7e6a521231 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -706,6 +706,7 @@ DropSubscriptionStmt
 DropTableSpaceStmt
 DropUserMappingStmt
 DropdbStmt
+DropInvalidIndexesStmt
 DumpComponents
 DumpId
 DumpOptions
-- 
2.50.1 (Apple Git-155)

