From 701eadb71564fddb8e7fe00e11bb8d22f57f7edd Mon Sep 17 00:00:00 2001
From: Ian Barwick <barwick@gmail.com>
Date: Fri, 30 Oct 2026 15:39:34 +0900
Subject: [PATCH v1] Support ALTER FOREIGN TABLE ... RENAME CONSTRAINT ...

ALTER FOREIGN TABLE already provides support to add and drop
constraints. Enable the renaming existing constraints for
consistency with ALTER TABLE.

Some refactoring is applied to "AlterObjectTypeCommandTag()" to
ensure the correct command tag (ALTER TABLE or ALTER FOREIGN
TABLE) is returned for an ALTER [COLUMN|CONSTRAINT] operation on
either table type.

psql tab completion is added for ALTER FOREIGN TABLE ... RENAME CONSTRAINT,
and also for ADD/DROP COLUMN/CONSTRAINT, as the infrastructure to
support those was pretty much there anyway.
---
 doc/src/sgml/ref/alter_foreign_table.sgml  |  6 +++--
 src/backend/parser/gram.y                  | 26 +++++++++++++++++++
 src/backend/tcop/utility.c                 | 29 ++++++++++++++--------
 src/bin/psql/tab-complete.in.c             | 21 ++++++++++++++++
 src/test/regress/expected/foreign_data.out |  7 +++++-
 src/test/regress/sql/foreign_data.sql      |  5 +++-
 6 files changed, 79 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index 228067f087c..412c19177a1 100644
--- a/doc/src/sgml/ref/alter_foreign_table.sgml
+++ b/doc/src/sgml/ref/alter_foreign_table.sgml
@@ -25,6 +25,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] [ ONLY ] <replaceable class="parameter">name</
     <replaceable class="parameter">action</replaceable> [, ... ]
 ALTER FOREIGN TABLE [ IF EXISTS ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
     RENAME [ COLUMN ] <replaceable class="parameter">column_name</replaceable> TO <replaceable class="parameter">new_column_name</replaceable>
+ALTER FOREIGN TABLE [ IF EXISTS ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
+    RENAME CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> TO <replaceable class="parameter">new_constraint_name</replaceable>
 ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     RENAME TO <replaceable class="parameter">new_name</replaceable>
 ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
@@ -288,8 +290,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceab
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
-      The <literal>RENAME</literal> forms change the name of a foreign table
-      or the name of an individual column in a foreign table.
+      The <literal>RENAME</literal> forms change the name of a foreign table,
+      or the name of an individual column or constraint in a foreign table.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..6caf03a7e0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9940,6 +9940,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					RenameStmt *n = makeNode(RenameStmt);
 
 					n->renameType = OBJECT_TABCONSTRAINT;
+					n->relationType = OBJECT_TABLE;
 					n->relation = $3;
 					n->subname = $6;
 					n->newname = $8;
@@ -9951,12 +9952,37 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					RenameStmt *n = makeNode(RenameStmt);
 
 					n->renameType = OBJECT_TABCONSTRAINT;
+					n->relationType = OBJECT_TABLE;
 					n->relation = $5;
 					n->subname = $8;
 					n->newname = $10;
 					n->missing_ok = true;
 					$$ = (Node *) n;
 				}
+			| ALTER FOREIGN TABLE relation_expr RENAME CONSTRAINT name TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_TABCONSTRAINT;
+					n->relationType = OBJECT_FOREIGN_TABLE;
+					n->relation = $4;
+					n->subname = $7;
+					n->newname = $9;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER FOREIGN TABLE IF_P EXISTS relation_expr RENAME CONSTRAINT name TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_TABCONSTRAINT;
+					n->relationType = OBJECT_FOREIGN_TABLE;
+					n->relation = $6;
+					n->subname = $9;
+					n->newname = $11;
+					n->missing_ok = true;
+					$$ = (Node *) n;
+				}
 			| ALTER FOREIGN TABLE relation_expr RENAME opt_column name TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 1512d2df196..70222f20a67 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2241,6 +2241,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 {
 	CommandTag	tag;
 
+	/*
+	 * COLUMN and CONSTRAINT modifications should always return either
+	 * ALTER TABLE or ALTER FOREIGN TABLE.
+	 */
+	Assert(objtype != OBJECT_COLUMN && objtype != OBJECT_TABCONSTRAINT);
+
 	switch (objtype)
 	{
 		case OBJECT_AGGREGATE:
@@ -2255,9 +2261,6 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLLATION:
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
-		case OBJECT_COLUMN:
-			tag = CMDTAG_ALTER_TABLE;
-			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2323,7 +2326,6 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 			tag = CMDTAG_ALTER_SEQUENCE;
 			break;
 		case OBJECT_TABLE:
-		case OBJECT_TABCONSTRAINT:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
 		case OBJECT_TABLESPACE:
@@ -2695,14 +2697,19 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_RenameStmt:
+			{
+				ObjectType objtype = ((RenameStmt *) parsetree)->renameType;
 
-			/*
-			 * When the column is renamed, the command tag is created from its
-			 * relation type
-			 */
-			tag = AlterObjectTypeCommandTag(((RenameStmt *) parsetree)->renameType == OBJECT_COLUMN ?
-											((RenameStmt *) parsetree)->relationType :
-											((RenameStmt *) parsetree)->renameType);
+				/*
+				 * When a column or constraint is renamed, the command tag is
+				 * created from its relation type. Otherwise, the command tag
+				 * is created from the object type.
+				 */
+				if (objtype == OBJECT_COLUMN || objtype == OBJECT_TABCONSTRAINT)
+					objtype = ((RenameStmt *) parsetree)->relationType;
+
+				tag = AlterObjectTypeCommandTag(objtype);
+			}
 			break;
 
 		case T_AlterObjectDependsStmt:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 96de78cd087..924d1e9a6c7 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2475,6 +2475,27 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("ADD", "ALTER", "DISABLE TRIGGER", "DROP", "ENABLE",
 					  "INHERIT", "NO INHERIT", "OPTIONS", "OWNER TO",
 					  "RENAME", "SET", "VALIDATE CONSTRAINT");
+	/* ALTER FOREIGN TABLE <name> RENAME (TO|COLUMN|CONSTRAINT) */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "RENAME"))
+		COMPLETE_WITH_ATTR_PLUS(prev2_wd, "TO", "COLUMN", "CONSTRAINT");
+	/* ALTER FOREIGN TABLE <name> RENAME <column>, provide "TO" */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "RENAME", MatchAnyExcept("COLUMN|CONSTRAINT|TO")))
+		COMPLETE_WITH("TO");
+	/* ALTER FOREIGN TABLE <name> RENAME (COLUMN|CONSTRAINT) <sth>, provide "TO" */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "RENAME", "COLUMN|CONSTRAINT", MatchAnyExcept("TO")))
+		COMPLETE_WITH("TO");
+	/* ALTER FOREIGN TABLE <name> (ADD|DROP), provide "COLUMN" or "CONSTRAINT" */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "ADD|DROP"))
+		COMPLETE_WITH("COLUMN", "CONSTRAINT");
+	/* ALTER FOREIGN TABLE <name> (DROP|RENAME) COLUMN, provide list of columns */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "DROP|RENAME", "COLUMN"))
+		COMPLETE_WITH_ATTR(prev3_wd);
+	/* ALTER FOREIGN TABLE <name> (DROP|RENAME) CONSTRAINT, provide list of constraints */
+	else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny, "DROP|RENAME", "CONSTRAINT"))
+	{
+		set_completion_reference(prev3_wd);
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table);
+	}
 
 	/* ALTER INDEX */
 	else if (Matches("ALTER", "INDEX"))
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index d8e4cb12c3d..56bc5e7684e 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -905,7 +905,10 @@ ALTER FOREIGN TABLE ft1 ADD CONSTRAINT ft1_c9_check CHECK (c9 < 0) NOT VALID;
 ALTER FOREIGN TABLE ft1 ALTER CONSTRAINT ft1_c9_check DEFERRABLE; -- ERROR
 ERROR:  ALTER action ALTER CONSTRAINT cannot be performed on relation "ft1"
 DETAIL:  This operation is not supported for foreign tables.
-ALTER FOREIGN TABLE ft1 DROP CONSTRAINT ft1_c9_check;
+ALTER FOREIGN TABLE ft1 RENAME CONSTRAINT ft1_c9_check_1 TO ft1_c9_check; -- ERROR
+ERROR:  constraint "ft1_c9_check_1" for table "ft1" does not exist
+ALTER FOREIGN TABLE ft1 RENAME CONSTRAINT ft1_c9_check TO ft1_c9_check_1;
+ALTER FOREIGN TABLE ft1 DROP CONSTRAINT ft1_c9_check_1;
 ALTER FOREIGN TABLE ft1 DROP CONSTRAINT no_const;               -- ERROR
 ERROR:  constraint "no_const" of relation "ft1" does not exist
 ALTER FOREIGN TABLE ft1 DROP CONSTRAINT IF EXISTS no_const;
@@ -977,6 +980,8 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 OPTIONS (ADD p1 '
 NOTICE:  relation "doesnt_exist_ft1" does not exist, skipping
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
 NOTICE:  relation "doesnt_exist_ft1" does not exist, skipping
+ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME CONSTRAINT no_const TO no_const_1;
+NOTICE:  relation "doesnt_exist_ft1" does not exist, skipping
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT IF EXISTS no_const;
 NOTICE:  relation "doesnt_exist_ft1" does not exist, skipping
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT ft1_c1_check;
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index ed78052b1e2..ada87aa8327 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -425,7 +425,9 @@ DROP TABLE use_ft1_column_type;
 ALTER FOREIGN TABLE ft1 ADD PRIMARY KEY (c7);                   -- ERROR
 ALTER FOREIGN TABLE ft1 ADD CONSTRAINT ft1_c9_check CHECK (c9 < 0) NOT VALID;
 ALTER FOREIGN TABLE ft1 ALTER CONSTRAINT ft1_c9_check DEFERRABLE; -- ERROR
-ALTER FOREIGN TABLE ft1 DROP CONSTRAINT ft1_c9_check;
+ALTER FOREIGN TABLE ft1 RENAME CONSTRAINT ft1_c9_check_1 TO ft1_c9_check; -- ERROR
+ALTER FOREIGN TABLE ft1 RENAME CONSTRAINT ft1_c9_check TO ft1_c9_check_1;
+ALTER FOREIGN TABLE ft1 DROP CONSTRAINT ft1_c9_check_1;
 ALTER FOREIGN TABLE ft1 DROP CONSTRAINT no_const;               -- ERROR
 ALTER FOREIGN TABLE ft1 DROP CONSTRAINT IF EXISTS no_const;
 ALTER FOREIGN TABLE ft1 OWNER TO regress_test_role;
@@ -460,6 +462,7 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 OPTIONS (ADD p1 '
                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
 
+ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME CONSTRAINT no_const TO no_const_1;
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT IF EXISTS no_const;
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT ft1_c1_check;
 ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OWNER TO regress_test_role;
-- 
2.54.0 (Apple Git-157)

