BUG #19723: CREATE INDEX racing with ALTER INDEX ATTACH PARTITION triggers unexpected internal error

Started by PG Bug reporting form27 days ago4 messagesbugs
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253954
psql -h localhost -U postgres

Built from patchset v4 (message #4), October 23, 2026 at 01:25 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253954_4 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253954_4 && git checkout t253954_4

Patchset v4 (message #4) is on t253954_4

Jump to latest
#1PG Bug reporting form
noreply@postgresql.org

The following bug has been logged on the website:

Bug reference: 19723
Logged by: Alexander Lakhin
Email address: exclusion@gmail.com
PostgreSQL version: 19beta4
Operating system: Ubuntu 24.04
Description:

The following script:
echo "
CREATE TABLE t (a int, b int) PARTITION BY list (b);
CREATE TABLE tp1 PARTITION OF t FOR VALUES IN (1);
" | psql

for ((i=1;i<=100;i++)); do
echo "iteration $i"
echo "
DROP INDEX t_a_idx;
DROP INDEX t_a_idx2;
CREATE INDEX t_a_idx ON ONLY t (a);
CREATE INDEX tp1_a_idx ON tp1 (a);
" | psql

echo "ALTER INDEX t_a_idx ATTACH PARTITION tp1_a_idx;" | psql &
echo "CREATE INDEX t_a_idx2 ON t(a);" | psql
wait
grep 'ERROR: bogus pg_inherit row' server.log && break;
done

tirggers:
iteration 3
DROP INDEX
DROP INDEX
CREATE INDEX
CREATE INDEX
ERROR: bogus pg_inherit row: inhrelid 16400 inhparent 16399
ALTER INDEX
2026-09-26 04:36:22.653 EDT|user|regression|6ab78406.1ddce8|XX000 ERROR:
bogus pg_inherit row: inhrelid 16400 inhparent 16399

which is described as unexpected:
/*
* A pg_inherits row exists. If it's the same we
want, then we're
* good; if it differs, that amounts to a corrupt
catalog and
* should not happen.
*/
if (inhForm->inhparent != parentOid)
{
/* unexpected: we should not get called in
this case */
elog(ERROR, "bogus pg_inherit row: inhrelid
%u inhparent %u",
inhForm->inhrelid,
inhForm->inhparent);
}

Reproduced starting from 8b08f7d48.

#2Ayush Tiwari
ayushtiwari.slg01@gmail.com
In reply to: PG Bug reporting form (#1)
Re: BUG #19723: CREATE INDEX racing with ALTER INDEX ATTACH PARTITION triggers unexpected internal error

Hi,

On Sat, 26 Sept 2026 at 18:51, PG Bug reporting form
<noreply@postgresql.org> wrote:

The following bug has been logged on the website:

Bug reference: 19723
Logged by: Alexander Lakhin
Email address: exclusion@gmail.com
PostgreSQL version: 19beta4
Operating system: Ubuntu 24.04
Description:

The following script:
echo "
CREATE TABLE t (a int, b int) PARTITION BY list (b);
CREATE TABLE tp1 PARTITION OF t FOR VALUES IN (1);
" | psql

for ((i=1;i<=100;i++)); do
echo "iteration $i"
echo "
DROP INDEX t_a_idx;
DROP INDEX t_a_idx2;
CREATE INDEX t_a_idx ON ONLY t (a);
CREATE INDEX tp1_a_idx ON tp1 (a);
" | psql

echo "ALTER INDEX t_a_idx ATTACH PARTITION tp1_a_idx;" | psql &
echo "CREATE INDEX t_a_idx2 ON t(a);" | psql
wait
grep 'ERROR: bogus pg_inherit row' server.log && break;
done

tirggers:
iteration 3
DROP INDEX
DROP INDEX
CREATE INDEX
CREATE INDEX
ERROR: bogus pg_inherit row: inhrelid 16400 inhparent 16399
ALTER INDEX
2026-09-26 04:36:22.653 EDT|user|regression|6ab78406.1ddce8|XX000 ERROR:
bogus pg_inherit row: inhrelid 16400 inhparent 16399

which is described as unexpected:
/*
* A pg_inherits row exists. If it's the same we
want, then we're
* good; if it differs, that amounts to a corrupt
catalog and
* should not happen.
*/
if (inhForm->inhparent != parentOid)
{
/* unexpected: we should not get called in
this case */
elog(ERROR, "bogus pg_inherit row: inhrelid
%u inhparent %u",
inhForm->inhrelid,
inhForm->inhparent);
}

Reproduced starting from 8b08f7d48.

Thanks for the report with repro and bisect.

I think I see how this happens. DefineIndex() calls has_superclass()
before locking the child index, although its comment says the caller
*must hold that lock*. If ALTER INDEX ... ATTACH hasn't committed yet,
CREATE INDEX sees the child as unattached, waits in index_open(), and
then tries to attach it to its new parent after ATTACH commits.

Would it make sense to open the index before calling has_superclass()?
Then the check would see the attachment after the wait and skip that
index.

The below simple diff fixed the issue for me:

---
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 5a0312fe772..561dd124e2c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1453,11 +1453,14 @@ DefineIndex(ParseState *pstate,
  Relation cldidx;
  IndexInfo  *cldIdxInfo;
+ cldidx = index_open(cldidxid, lockmode);
  /* this index is already partition of another one */
  if (has_superclass(cldidxid))
+ {
+ index_close(cldidx, lockmode);
  continue;
+ }

- cldidx = index_open(cldidxid, lockmode);
cldIdxInfo = BuildIndexInfo(cldidx);

Regards,
Ayush

#3Ayush Tiwari
ayushtiwari.slg01@gmail.com
In reply to: Ayush Tiwari (#2)
Re: BUG #19723: CREATE INDEX racing with ALTER INDEX ATTACH PARTITION triggers unexpected internal error

Hi,

On Sat, 26 Sept 2026 at 19:55, Ayush Tiwari <ayushtiwari.slg01@gmail.com> wrote:

On Sat, 26 Sept 2026 at 18:51, PG Bug reporting form
<noreply@postgresql.org> wrote:

The following bug has been logged on the website:

Bug reference: 19723
Logged by: Alexander Lakhin
Email address: exclusion@gmail.com
PostgreSQL version: 19beta4
Operating system: Ubuntu 24.04
Description:

The following script:
echo "
CREATE TABLE t (a int, b int) PARTITION BY list (b);
CREATE TABLE tp1 PARTITION OF t FOR VALUES IN (1);
" | psql

for ((i=1;i<=100;i++)); do
echo "iteration $i"
echo "
DROP INDEX t_a_idx;
DROP INDEX t_a_idx2;
CREATE INDEX t_a_idx ON ONLY t (a);
CREATE INDEX tp1_a_idx ON tp1 (a);
" | psql

echo "ALTER INDEX t_a_idx ATTACH PARTITION tp1_a_idx;" | psql &
echo "CREATE INDEX t_a_idx2 ON t(a);" | psql
wait
grep 'ERROR: bogus pg_inherit row' server.log && break;
done

tirggers:
iteration 3
DROP INDEX
DROP INDEX
CREATE INDEX
CREATE INDEX
ERROR: bogus pg_inherit row: inhrelid 16400 inhparent 16399
ALTER INDEX
2026-09-26 04:36:22.653 EDT|user|regression|6ab78406.1ddce8|XX000 ERROR:
bogus pg_inherit row: inhrelid 16400 inhparent 16399

which is described as unexpected:
/*
* A pg_inherits row exists. If it's the same we
want, then we're
* good; if it differs, that amounts to a corrupt
catalog and
* should not happen.
*/
if (inhForm->inhparent != parentOid)
{
/* unexpected: we should not get called in
this case */
elog(ERROR, "bogus pg_inherit row: inhrelid
%u inhparent %u",
inhForm->inhrelid,
inhForm->inhparent);
}

Reproduced starting from 8b08f7d48.

Thanks for the report with repro and bisect.

I think I see how this happens. DefineIndex() calls has_superclass()
before locking the child index, although its comment says the caller
*must hold that lock*. If ALTER INDEX ... ATTACH hasn't committed yet,
CREATE INDEX sees the child as unattached, waits in index_open(), and
then tries to attach it to its new parent after ATTACH commits.

Would it make sense to open the index before calling has_superclass()?
Then the check would see the attachment after the wait and skip that
index.

The below simple diff fixed the issue for me:

---
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 5a0312fe772..561dd124e2c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1453,11 +1453,14 @@ DefineIndex(ParseState *pstate,
Relation cldidx;
IndexInfo  *cldIdxInfo;
+ cldidx = index_open(cldidxid, lockmode);
/* this index is already partition of another one */
if (has_superclass(cldidxid))
+ {
+ index_close(cldidx, lockmode);
continue;
+ }

- cldidx = index_open(cldidxid, lockmode);
cldIdxInfo = BuildIndexInfo(cldidx);

Post some more testing, attaching patch file with above diff.

Regards,
Ayush

Attachments:

t253954_3
v1-0001-Lock-child-index-before-checking-its-parent.patchapplication/octet-stream; name=v1-0001-Lock-child-index-before-checking-its-parent.patchDownload+4-2
#4Rafia Sabih
rafia.pghackers@gmail.com
In reply to: Ayush Tiwari (#3)
Re: BUG #19723: CREATE INDEX racing with ALTER INDEX ATTACH PARTITION triggers unexpected internal error

On Mon, 28 Sept 2026 at 06:44, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
wrote:

Hi,

On Sat, 26 Sept 2026 at 19:55, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
wrote:

On Sat, 26 Sept 2026 at 18:51, PG Bug reporting form
<noreply@postgresql.org> wrote:

The following bug has been logged on the website:

Bug reference: 19723
Logged by: Alexander Lakhin
Email address: exclusion@gmail.com
PostgreSQL version: 19beta4
Operating system: Ubuntu 24.04
Description:

The following script:
echo "
CREATE TABLE t (a int, b int) PARTITION BY list (b);
CREATE TABLE tp1 PARTITION OF t FOR VALUES IN (1);
" | psql

for ((i=1;i<=100;i++)); do
echo "iteration $i"
echo "
DROP INDEX t_a_idx;
DROP INDEX t_a_idx2;
CREATE INDEX t_a_idx ON ONLY t (a);
CREATE INDEX tp1_a_idx ON tp1 (a);
" | psql

echo "ALTER INDEX t_a_idx ATTACH PARTITION tp1_a_idx;" | psql &
echo "CREATE INDEX t_a_idx2 ON t(a);" | psql
wait
grep 'ERROR: bogus pg_inherit row' server.log && break;
done

tirggers:
iteration 3
DROP INDEX
DROP INDEX
CREATE INDEX
CREATE INDEX
ERROR: bogus pg_inherit row: inhrelid 16400 inhparent 16399
ALTER INDEX
2026-09-26 04:36:22.653 EDT|user|regression|6ab78406.1ddce8|XX000

ERROR:

bogus pg_inherit row: inhrelid 16400 inhparent 16399

which is described as unexpected:
/*
* A pg_inherits row exists. If it's the same

we

want, then we're
* good; if it differs, that amounts to a

corrupt

catalog and
* should not happen.
*/
if (inhForm->inhparent != parentOid)
{
/* unexpected: we should not get

called in

this case */
elog(ERROR, "bogus pg_inherit row:

inhrelid

%u inhparent %u",
inhForm->inhrelid,
inhForm->inhparent);
}

Reproduced starting from 8b08f7d48.

Thanks for the report with repro and bisect.

I think I see how this happens. DefineIndex() calls has_superclass()
before locking the child index, although its comment says the caller
*must hold that lock*. If ALTER INDEX ... ATTACH hasn't committed yet,
CREATE INDEX sees the child as unattached, waits in index_open(), and
then tries to attach it to its new parent after ATTACH commits.

Would it make sense to open the index before calling has_superclass()?
Then the check would see the attachment after the wait and skip that
index.

The below simple diff fixed the issue for me:

---
diff --git a/src/backend/commands/indexcmds.c

b/src/backend/commands/indexcmds.c

index 5a0312fe772..561dd124e2c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1453,11 +1453,14 @@ DefineIndex(ParseState *pstate,
Relation cldidx;
IndexInfo  *cldIdxInfo;
+ cldidx = index_open(cldidxid, lockmode);
/* this index is already partition of another one */
if (has_superclass(cldidxid))
+ {
+ index_close(cldidx, lockmode);
continue;
+ }

- cldidx = index_open(cldidxid, lockmode);
cldIdxInfo = BuildIndexInfo(cldidx);

Post some more testing, attaching patch file with above diff.

I went through this patch and the fix looks good to me. However I see one

problem with this in the following scenario,
s1: BEGIN; ALTER INDEX c_a_idx RENAME TO c_renamed; -- c_a_idx attached
s2: CREATE INDEX pi_new ON p (a); -- now waits (used to complete)
s1: INSERT INTO c VALUES (1);
=> ERROR: deadlock detected
Unpatched, both sessions complete.

So basically it is occurring because of opening the index first including
ones that are
already attached. Before the patch, those were skipped without taking any
lock.
That lock conflicts with locks other sessions can hold on an attached
index without conflicting on the partition itself, so CREATE INDEX on
the parent can now block, or deadlock, where it didn't before. What I
suggest is
we could keep the unlocked check as a fast path and check again only after
taking the lock:

/* this index is already partition of another one */
if (has_superclass(cldidxid))
continue;

cldidx = index_open(cldidxid, lockmode);

/* recheck, in case it was attached while we waited for the lock */
if (has_superclass(cldidxid))
{
index_close(cldidx, lockmode);
continue;
}

Another remark is to add an isolation test for this.
I have attached a patch with these additions.
--
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH

Attachments:

t253954_4
0001-Recheck-partition-index-attachment-after-locking-it.patchapplication/octet-stream; name=0001-Recheck-partition-index-attachment-after-locking-it.patchDownload+108-1