From 262f1f61a25d274d742ea4d5c02a93bdebfcf5f0 Mon Sep 17 00:00:00 2001 From: roman khapov Date: Tue, 30 Jun 2026 15:03:55 +0500 Subject: [PATCH v2] 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 Signed-off-by: roman khapov --- src/backend/catalog/index.c | 6 ++ src/backend/commands/tablecmds.c | 81 +++++++++++++++++++ src/backend/parser/gram.y | 26 +++++- 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 + .../test_misc/t/014_drop_invalid_indexes.pl | 79 ++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 11 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 src/test/modules/test_misc/t/014_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/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..474c3ae070b 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,26 @@ ReassignOwnedStmt: } ; +/***************************************************************************** + * + * QUERY: + * + * DROP INVALID INDEXES [ CONCURRENTLY ] ON TABLE tablename [, tablename ...] [ RESTRICT | CASCADE ] + * + *****************************************************************************/ + +DropInvalidIndexesStmt: + DROP INVALID_P INDEXES opt_concurrently ON TABLE any_name_list opt_drop_behavior + { + DropInvalidIndexesStmt *n = makeNode(DropInvalidIndexesStmt); + + n->tablenames = $7; + n->behavior = $8; + n->concurrent = $4; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -18958,6 +18979,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..a9f1004f3ff 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/test/modules/test_misc/t/014_drop_invalid_indexes.pl b/src/test/modules/test_misc/t/014_drop_invalid_indexes.pl new file mode 100644 index 00000000000..c8ee8acd9e8 --- /dev/null +++ b/src/test/modules/test_misc/t/014_drop_invalid_indexes.pl @@ -0,0 +1,79 @@ +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('ddl_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 ddl_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 = 'ddl_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{ + DROP INVALID INDEXES ON TABLE tt; + } +); + +is( $node->safe_psql( + 'postgres', + "select count(1) from pg_index where not indisvalid;"), + '0', + 'dropped invalid index'); + + +done_testing(); + + + + diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3a2720fb5f9..435c041ab1d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -707,6 +707,7 @@ DropSubscriptionStmt DropTableSpaceStmt DropUserMappingStmt DropdbStmt +DropInvalidIndexesStmt DumpComponents DumpId DumpOptions -- 2.43.0