Re: tab completion of enum values is broken - Mailing list pgsql-hackers

From Tom Lane
Subject Re: tab completion of enum values is broken
Date
Msg-id 3430513.1642200519@sss.pgh.pa.us
Whole thread Raw
In response to Re: tab completion of enum values is broken  (Tom Lane <tgl@sss.pgh.pa.us>)
List pgsql-hackers
I wrote:
> Oh ... experimenting on macOS (with the system-provided libedit)
> shows no bug here.  So I guess we'll need to make this conditional
> somehow, perhaps on USE_FILENAME_QUOTING_FUNCTIONS.  That's another
> reason for not going overboard.

After further fooling with that, I concluded that the only workable
solution is a run-time check for whether the readline library included
the leading quote in what it hands us.  A big advantage of doing it this
way is that it mostly fixes my complaint #1: by crafting the check
properly, we will include quotes if the user hits TAB without having typed
anything, and we won't complete an incorrectly non-quoted identifier.
There's still nothing to be done about single quotes inside an enum
label, but I'm okay with blowing that case off.

So I think the attached is committable.  I've tried it on readline
7.0 (RHEL8) as well as whatever libedit Apple is currently shipping.

            regards, tom lane

diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index ba98b8190b..d3d1bd650e 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -42,7 +42,8 @@ $node->start;
 $node->safe_psql('postgres',
         "CREATE TABLE tab1 (f1 int, f2 text);\n"
       . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
-      . "CREATE TABLE mytab246 (f1 int, f2 text);\n");
+      . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
+      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz');\n");

 # Developers would not appreciate this test adding a bunch of junk to
 # their ~/.psql_history, so be sure to redirect history into a temp file.
@@ -223,6 +224,16 @@ check_completion(

 clear_line();

+# check enum label completion
+# some versions of readline/libedit require two tabs here, some only need one
+# also, some versions will offer quotes, some will not
+check_completion(
+    "ALTER TYPE enum1 RENAME VALUE 'ba\t\t",
+    qr|'?bar'? +'?baz'?|,
+    "offer multiple enum choices");
+
+clear_line();
+
 # send psql an explicit \q to shut it down, else pty won't close properly
 $timer->start(5);
 $in .= "\\q\n";
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 39be6f556a..f7ce90da6e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -278,25 +278,40 @@ do { \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

+/*
+ * libedit will typically include the literal's leading single quote in
+ * "text", while readline will not.  Adapt our offered strings to fit.
+ * If we don't yet have text, include quotes in what's offered, to get the
+ * user off to the right start.
+ */
 #define COMPLETE_WITH_ENUM_VALUE(type) \
 do { \
     char   *_completion_schema; \
     char   *_completion_type; \
+    bool    use_quotes; \
 \
     _completion_schema = strtokx(type, " \t\n\r", ".", "\"", 0, \
                                  false, false, pset.encoding); \
     (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
                    false, false, pset.encoding); \
     _completion_type = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                               false, false, pset.encoding);  \
-    if (_completion_type == NULL)\
+                               false, false, pset.encoding); \
+    use_quotes = (text[0] == '\'' || \
+                  start == 0 || rl_line_buffer[start - 1] != '\''); \
+    if (_completion_type == NULL) \
     { \
-        completion_charp = Query_for_list_of_enum_values; \
+        if (use_quotes) \
+            completion_charp = Query_for_list_of_enum_values_quoted; \
+        else \
+            completion_charp = Query_for_list_of_enum_values_unquoted; \
         completion_info_charp = type; \
     } \
     else \
     { \
-        completion_charp = Query_for_list_of_enum_values_with_schema; \
+        if (use_quotes) \
+            completion_charp = Query_for_list_of_enum_values_with_schema_quoted; \
+        else \
+            completion_charp = Query_for_list_of_enum_values_with_schema_unquoted; \
         completion_info_charp = _completion_type; \
         completion_info_charp2 = _completion_schema; \
     } \
@@ -678,7 +693,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND (pg_catalog.quote_ident(nspname)='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

-#define Query_for_list_of_enum_values \
+#define Query_for_list_of_enum_values_quoted \
 "SELECT pg_catalog.quote_literal(enumlabel) "\
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
 " WHERE t.oid = e.enumtypid "\
@@ -687,7 +702,16 @@ static const SchemaQuery Query_for_list_of_collations = {
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

-#define Query_for_list_of_enum_values_with_schema \
+#define Query_for_list_of_enum_values_unquoted \
+"SELECT enumlabel "\
+"  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
+" WHERE t.oid = e.enumtypid "\
+"   AND substring(enumlabel,1,%d)='%s' "\
+"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"        OR '\"' || typname || '\"'='%s') "\
+"   AND pg_catalog.pg_type_is_visible(t.oid)"
+
+#define Query_for_list_of_enum_values_with_schema_quoted \
 "SELECT pg_catalog.quote_literal(enumlabel) "\
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t, pg_catalog.pg_namespace n "\
 " WHERE t.oid = e.enumtypid "\
@@ -698,6 +722,17 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND (pg_catalog.quote_ident(nspname)='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

+#define Query_for_list_of_enum_values_with_schema_unquoted \
+"SELECT enumlabel "\
+"  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t, pg_catalog.pg_namespace n "\
+" WHERE t.oid = e.enumtypid "\
+"   AND n.oid = t.typnamespace "\
+"   AND substring(enumlabel,1,%d)='%s' "\
+"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"        OR '\"' || typname || '\"'='%s') "\
+"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"        OR '\"' || nspname || '\"' ='%s') "
+
 #define Query_for_list_of_template_databases \
 "SELECT pg_catalog.quote_ident(d.datname) "\
 "  FROM pg_catalog.pg_database d "\
@@ -4413,8 +4448,12 @@ psql_completion(const char *text, int start, int end)
     if (matches == NULL)
     {
         COMPLETE_WITH_CONST(true, "");
+        /* Also, prevent Readline from appending stuff to the non-match */
 #ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
         rl_completion_append_character = '\0';
+#endif
+#ifdef HAVE_RL_COMPLETION_SUPPRESS_QUOTE
+        rl_completion_suppress_quote = 1;
 #endif
     }


pgsql-hackers by date:

Previous
From: Andres Freund
Date:
Subject: Re: Why is src/test/modules/committs/t/002_standby.pl flaky?
Next
From: "Kingsborough, Alex"
Date:
Subject: Null commitTS bug