From c5c942cd2595d78ff3d97eb5378aac722e806ac9 Mon Sep 17 00:00:00 2001
From: Peter Smith <peter.b.smith@fujitsu.com>
Date: Wed, 29 Jul 2026 16:53:28 +1000
Subject: [PATCH v2] psql - tab-completion of pub/sub options.

For commands:

CREATE PUBLICATION ... WITH (options)
ALTER PUBLICATION ... SET (options)

CREATE SUBSCRIPTION ... WITH (options)
ALTER SUBSCRIPTION ... SET (options)

Now:
- handles multiple options of the WITH/SET comma-separated lists
- enum options show their allowed values

Author: Peter Smith <smithpb2250@gmail.com>
Reviewed-by: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>

Discussion: https://www.postgresql.org/message-id/flat/OS9PR01MB121493EC5F804CBF8BC5E47F7F5CB2%40OS9PR01MB12149.jpnprd01.prod.outlook.com#3e9996f9509f977031fbb94c910600aa
---
 src/bin/psql/tab-complete.in.c | 170 +++++++++++++++++++++++++++++----
 1 file changed, 150 insertions(+), 20 deletions(-)

diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1cacc8c3ea2..6d30f882572 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2341,9 +2341,24 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
 								 " AND nspname NOT LIKE E'pg\\\\_%%'",
 								 "CURRENT_SCHEMA");
-	/* ALTER PUBLICATION <name> SET ( */
-	else if (Matches("ALTER", "PUBLICATION", MatchAny, MatchAnyN, "SET", "("))
-		COMPLETE_WITH("publish", "publish_generated_columns", "publish_via_partition_root");
+	/* ALTER PUBLICATION <name> SET ( <opt> */
+	else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "SET", "(*") &&
+			!HeadMatches("ALTER", "PUBLICATION", MatchAny, "SET", "(*)"))
+	{
+		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+			COMPLETE_WITH("publish =", "publish_generated_columns =",
+						  "publish_via_partition_root");
+
+			/* Complete "ALTER PUBLICATION <name> SET ( <opt> =" */
+
+			else if (TailMatches("publish"))
+				COMPLETE_WITH("=");
+
+			else if (TailMatches("publish_generated_columns"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("publish_generated_columns", "="))
+				COMPLETE_WITH("none", "stored");
+	}
 	/* ALTER SUBSCRIPTION <name> */
 	else if (Matches("ALTER", "SUBSCRIPTION", MatchAny))
 		COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO",
@@ -2361,13 +2376,49 @@ match_previous_words(int pattern_id,
 	else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
 		COMPLETE_WITH("(", "PUBLICATION");
 	/* ALTER SUBSCRIPTION <name> SET ( */
-	else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "SET", "("))
-		COMPLETE_WITH("binary", "conflict_log_destination", "disable_on_error",
-					  "failover", "max_retention_duration", "origin",
+	else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny, "SET", "(*") &&
+			!HeadMatches("ALTER", "SUBSCRIPTION", MatchAny, "SET", "(*)"))
+	{
+		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+			COMPLETE_WITH("binary", "conflict_log_destination =",
+					  "disable_on_error", "failover",
+					  "max_retention_duration =", "origin =",
 					  "password_required", "retain_dead_tuples",
-					  "run_as_owner", "slot_name", "streaming",
-					  "synchronous_commit", "two_phase",
-					  "wal_receiver_timeout");
+					  "run_as_owner", "slot_name =", "streaming =",
+					  "synchronous_commit =", "two_phase",
+					  "wal_receiver_timeout =");
+
+		/* ALTER SUBSCRIPTION <name> SET ( <opt> = */
+
+		else if (TailMatches("conflict_log_destination"))
+			COMPLETE_WITH("=");
+		else if (TailMatches("conflict_log_destination", "="))
+			COMPLETE_WITH("all", "log", "table");
+
+		else if (TailMatches("max_retention_duration"))
+			COMPLETE_WITH("=");
+
+		else if (TailMatches("origin"))
+			COMPLETE_WITH("=");
+		else if (TailMatches("origin", "="))
+			COMPLETE_WITH("any", "none");
+
+		else if (TailMatches("slot_name"))
+			COMPLETE_WITH("=");
+
+		else if (TailMatches("streaming"))
+			COMPLETE_WITH("=");
+		else if (TailMatches("streaming", "="))
+			COMPLETE_WITH("off", "on", "parallel");
+
+		else if (TailMatches("synchronous_commit"))
+			COMPLETE_WITH("=");
+		else if (TailMatches("synchronous_commit", "="))
+			COMPLETE_WITH("local", "off", "on", "remote_apply", "remote_write");
+
+		else if (TailMatches("wal_receiver_timeout"))
+			COMPLETE_WITH("=");
+	}
 	/* ALTER SUBSCRIPTION <name> SKIP ( */
 	else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "SKIP", "("))
 		COMPLETE_WITH("lsn");
@@ -3776,9 +3827,37 @@ match_previous_words(int pattern_id,
 								 "CURRENT_SCHEMA");
 	else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES", "IN", "SCHEMA", MatchAny) && (!ends_with(prev_wd, ',')))
 		COMPLETE_WITH("WITH (");
-	/* Complete "CREATE PUBLICATION <name> [...] WITH" */
-	else if (Matches("CREATE", "PUBLICATION", MatchAnyN, "WITH", "("))
-		COMPLETE_WITH("publish", "publish_generated_columns", "publish_via_partition_root");
+	/* Complete "CREATE PUBLICATION <name> ... WITH ( <opt>" */
+	else if (HeadMatches("CREATE", "PUBLICATION"))
+	{
+		bool seen_with = false;
+
+		for (int i = 0; i < previous_words_count; i++)
+		{
+			if (pg_strcasecmp(previous_words[i], "WITH") == 0)
+			{
+				seen_with = true;
+				break;
+			}
+		}
+
+		if (seen_with)
+		{
+			if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+				COMPLETE_WITH("publish =", "publish_generated_columns =",
+							  "publish_via_partition_root");
+
+			/* Complete "CREATE PUBLICATION <name> ...  WITH ( <opt> =" */
+
+			else if (TailMatches("publish"))
+				COMPLETE_WITH("=");
+
+			else if (TailMatches("publish_generated_columns"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("publish_generated_columns", "="))
+				COMPLETE_WITH("none", "stored");
+		}
+	}
 
 /* CREATE RULE */
 	/* Complete "CREATE [ OR REPLACE ] RULE <sth>" with "AS ON" */
@@ -3945,15 +4024,66 @@ match_previous_words(int pattern_id,
 	}
 	else if (Matches("CREATE", "SUBSCRIPTION", MatchAnyN, "PUBLICATION", MatchAny))
 		COMPLETE_WITH("WITH (");
+	else if (Matches("CREATE", "SUBSCRIPTION", MatchAnyN, "WITH"))
+		COMPLETE_WITH("(");
 	/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt>" */
-	else if (Matches("CREATE", "SUBSCRIPTION", MatchAnyN, "WITH", "("))
-		COMPLETE_WITH("binary", "conflict_log_destination", "connect", "copy_data",
-					  "create_slot", "disable_on_error", "enabled", "failover",
-					  "max_retention_duration", "origin",
-					  "password_required", "retain_dead_tuples",
-					  "run_as_owner", "slot_name", "streaming",
-					  "synchronous_commit", "two_phase",
-					  "wal_receiver_timeout");
+	else if (HeadMatches("CREATE", "SUBSCRIPTION"))
+	{
+		bool seen_with = false;
+
+		for (int i = 0; i < previous_words_count; i++)
+		{
+			if (pg_strcasecmp(previous_words[i], "WITH") == 0)
+			{
+				seen_with = true;
+				break;
+			}
+		}
+
+		if (seen_with)
+		{
+			if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+				COMPLETE_WITH("binary", "conflict_log_destination =",
+							  "connect", "copy_data", "create_slot",
+							  "disable_on_error", "enabled", "failover",
+							  "max_retention_duration =", "origin =",
+							  "password_required", "retain_dead_tuples",
+							  "run_as_owner", "slot_name =", "streaming =",
+							  "synchronous_commit =", "two_phase",
+							  "wal_receiver_timeout =");
+
+			/* Complete "CREATE SUBSCRIPTION <name> ...  WITH ( <opt> =" */
+
+			else if (TailMatches("conflict_log_destination"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("conflict_log_destination", "="))
+				COMPLETE_WITH("all", "log", "table");
+
+			else if (TailMatches("max_rentention_duration"))
+				COMPLETE_WITH("=");
+
+			else if (TailMatches("origin"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("origin", "="))
+				COMPLETE_WITH("any", "none");
+
+			else if (TailMatches("slot_name"))
+				COMPLETE_WITH("=");
+
+			else if (TailMatches("streaming"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("streaming", "="))
+				COMPLETE_WITH("off", "on", "parallel");
+
+			else if (TailMatches("synchronous_commit"))
+				COMPLETE_WITH("=");
+			else if (TailMatches("synchronous_commit", "="))
+				COMPLETE_WITH("local", "off", "on", "remote_apply", "remote_write");
+
+			else if (TailMatches("wal_receiver_timeout"))
+				COMPLETE_WITH("=");
+		}
+	}
 
 /* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
 
-- 
2.47.3

