From 1f35767b3c9e4ef682e472050012e931731e095e Mon Sep 17 00:00:00 2001 From: roman khapov 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 Signed-off-by: Roman Khapov --- 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)