From c664e035dacc5b161448c12866e0b5f5b4c0e01c Mon Sep 17 00:00:00 2001
From: Dhruv Chauhan <chauhandhruv351@gmail.com>
Date: Tue, 28 Jul 2026 13:53:12 +0530
Subject: [PATCH v2] Avoid empty generated names for expression index columns

When an index column is an expression, its name is generated from the
Vars, Consts, and function names found in the expression.  A whole-row
Var was skipped, and an expression yielding no usable text at all (for
example a constant made up only of punctuation) produced an empty name
-- giving an empty column name and an odd index name such as "t__idx".

Use the relation's name for a whole-row Var, and fall back to "expr"
when nothing usable is found, so the generated column name is never
empty.  Ordinary expressions are unaffected.
---
 src/backend/commands/indexcmds.c           | 26 ++++++++++++++++-----
 src/test/regress/expected/create_index.out | 27 ++++++++++++++++++++++
 src/test/regress/sql/create_index.sql      | 21 +++++++++++++++++
 3 files changed, 68 insertions(+), 6 deletions(-)

diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 713bb5d10f..0cd3b58e07 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';
@@ -2891,19 +2894,30 @@ ChooseIndexExpressionName_walker(Node *node,
 	{
 		Var		   *var = (Var *) node;
 		TupleDesc	tupdesc = RelationGetDescr(context->rel);
-		Form_pg_attribute att;
+		const char *name;
 
 		/* Paranoia: ignore the Var if it looks fishy */
 		if (var->varno != 1 || var->varlevelsup != 0 ||
-			var->varattno <= 0 || var->varattno > tupdesc->natts)
+			var->varattno < 0 || var->varattno > tupdesc->natts)
 			return false;
-		att = TupleDescAttr(tupdesc, var->varattno - 1);
-		if (att->attisdropped)
-			return false;		/* even more paranoia; shouldn't happen */
+
+		if (var->varattno == 0)
+		{
+			/* Whole-row reference: use the relation's name */
+			name = RelationGetRelationName(context->rel);
+		}
+		else
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, var->varattno - 1);
+
+			if (att->attisdropped)
+				return false;	/* even more paranoia; shouldn't happen */
+			name = NameStr(att->attname);
+		}
 
 		if (context->buf->len > 0)
 			appendStringInfoChar(context->buf, '_');
-		appendStringInfoString(context->buf, NameStr(att->attname));
+		appendStringInfoString(context->buf, name);
 
 		/* Done if we've already reached NAMEDATALEN */
 		return (context->buf->len >= NAMEDATALEN);
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 1145d57726..281c0f1355 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1648,6 +1648,33 @@ 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 for expressions.  A whole-row reference
+-- contributes the relation name; an expression that yields no usable text
+-- at all falls back to "expr", so the column name is never empty.
+--
+CREATE TABLE gen_exprname (a int, b int);
+-- whole-row reference uses the relation name
+CREATE INDEX ON gen_exprname ((gen_exprname IS NOT NULL));
+-- nothing usable to extract -> fall back to "expr"
+CREATE INDEX ON gen_exprname ((','::text));
+-- 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
+ gen_exprname_gen_exprname_idx | gen_exprname
+(3 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..002b8a0743 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -649,6 +649,27 @@ CREATE INDEX ON syscol_table (a) WHERE ctid >= '(1000,0)';
 
 DROP TABLE syscol_table;
 
+--
+-- Generated index column names for expressions.  A whole-row reference
+-- contributes the relation name; an expression that yields no usable text
+-- at all falls back to "expr", so the column name is never empty.
+--
+CREATE TABLE gen_exprname (a int, b int);
+-- whole-row reference uses the relation name
+CREATE INDEX ON gen_exprname ((gen_exprname IS NOT NULL));
+-- nothing usable to extract -> fall back to "expr"
+CREATE INDEX ON gen_exprname ((','::text));
+-- 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

