If you CREATE INDEX on an expression without naming the index, Postgres
generates a name for the index column from that expression.  For some
expressions the generated name comes out empty, so the index column is
left with an empty attname.

Example (a whole-row reference):

    CREATE TABLE t (a int, b int);
    CREATE INDEX ON t ((t IS NOT NULL));

    SELECT attname, length(attname)
      FROM pg_attribute
      WHERE attrelid = 't__idx'::regclass AND attnum = 1;
     attname | length
    ---------+--------
             |      0
    (1 row)

The index is named "t__idx" -- the doubled underscore is where the
empty column name landed.

Cause: ChooseIndexExpressionName() builds the name from the Vars,
Consts, and function names in the expression.  A whole-row Var is
skipped and a punctuation-only Const is stripped away, so such
expressions contribute no text and the result is empty.  Before commit
181b6185c7, expression columns were always named "expr" (giving
"t_expr_idx" here).

Fix (attached): if the walk finds nothing usable, fall back to "expr".
Ordinary expressions are unaffected.  Includes a regression test; make
check is green.  Against the master.

Patch is attached.

Thanks,
Dhruv Chauhan
From c545ab7c6373a8602a377509616b13b778e34ecd Mon Sep 17 00:00:00 2001
From: Dhruv Chauhan <[email protected]>
Date: Mon, 27 Jul 2026 21:11:03 +0530
Subject: [PATCH v1] Avoid generating an empty name for an expression index
 column

When an index column is an expression, its name is built from the
Vars, Consts, and function names found in the expression. Some
expressions contain none of these -- for example a whole-row
reference, or a constant made up entirely of punctuation -- so the
generated name came out empty. That produces an empty column name
and an awkward index name such as "t__idx".

Fall back to "expr" when the expression yields no usable text, so a
non-empty name is always produced.
---
 src/backend/commands/indexcmds.c           |  3 +++
 src/test/regress/expected/create_index.out | 22 ++++++++++++++++++++++
 src/test/regress/sql/create_index.sql      | 17 +++++++++++++++++
 3 files changed, 42 insertions(+)

diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 713bb5d10f..834c9e2275 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2874,6 +2874,9 @@ ChooseIndexExpressionName(Relation rel, Node *indexExpr)
 	context.buf = &buf;
 	/* Walk the tree, stopping when we have enough text */
 	(void) ChooseIndexExpressionName_walker(indexExpr, &context);
+	/* Fall back to "expr" if the walk found nothing, to avoid an empty name */
+	if (buf.len == 0)
+		appendStringInfoString(&buf, "expr");
 	/* Ensure generated names are shorter than NAMEDATALEN */
 	nlen = pg_mbcliplen(buf.data, buf.len, NAMEDATALEN - 1);
 	buf.data[nlen] = '\0';
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 1145d57726..d75683d37b 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1648,6 +1648,28 @@ CREATE INDEX ON syscol_table (a) WHERE ctid >= '(1000,0)';
 ERROR:  index creation on system columns is not supported
 DROP TABLE syscol_table;
 --
+-- Generated index column names must never be empty; fall back to "expr".
+--
+CREATE TABLE gen_exprname (a int, b int);
+-- whole-row expression yields no usable text -> "expr"
+CREATE INDEX ON gen_exprname ((gen_exprname IS NOT NULL));
+-- normal expression still gets a descriptive name
+CREATE INDEX ON gen_exprname ((a + b));
+SELECT c.relname AS index_name, a.attname AS column_name
+  FROM pg_class c
+  JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0
+  WHERE c.relkind = 'i'
+    AND c.relnamespace = 'public'::regnamespace
+    AND c.relname LIKE 'gen_exprname%'
+  ORDER BY c.relname, a.attnum;
+      index_name       | column_name 
+-----------------------+-------------
+ gen_exprname_a_b_idx  | a_b
+ gen_exprname_expr_idx | expr
+(2 rows)
+
+DROP TABLE gen_exprname;
+--
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
 CREATE TABLE onek_with_null AS SELECT unique1, unique2 FROM onek;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 8e59f6bcd0..5d0e6520da 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -649,6 +649,23 @@ CREATE INDEX ON syscol_table (a) WHERE ctid >= '(1000,0)';
 
 DROP TABLE syscol_table;
 
+--
+-- Generated index column names must never be empty; fall back to "expr".
+--
+CREATE TABLE gen_exprname (a int, b int);
+-- whole-row expression yields no usable text -> "expr"
+CREATE INDEX ON gen_exprname ((gen_exprname IS NOT NULL));
+-- normal expression still gets a descriptive name
+CREATE INDEX ON gen_exprname ((a + b));
+SELECT c.relname AS index_name, a.attname AS column_name
+  FROM pg_class c
+  JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0
+  WHERE c.relkind = 'i'
+    AND c.relnamespace = 'public'::regnamespace
+    AND c.relname LIKE 'gen_exprname%'
+  ORDER BY c.relname, a.attnum;
+DROP TABLE gen_exprname;
+
 --
 -- Tests for IS NULL/IS NOT NULL with b-tree indexes
 --
-- 
2.34.1

Reply via email to