Prepare tab-complete.c for preprocessing.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 7 Oct 2024 16:19:12 +0000 (12:19 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 7 Oct 2024 16:19:12 +0000 (12:19 -0400)
Separate out psql_completion's giant else-if chain of *Matches
tests into a new function.  Add the infrastructure needed for
table-driven checking of the initial match of each completion
rule.  As-is, however, the code continues to operate as it did.
The new behavior applies only if SWITCH_CONVERSION_APPLIED
is #defined, which it is not here.  (The preprocessor added
in the next patch will add a #define for that.)

The first and last couple of bits of psql_completion are not
based on HeadMatches/TailMatches/Matches tests, so they stay
where they are; they won't become part of the switch.

This patch also fixes up a couple of if-conditions that didn't meet
the conditions enumerated in the comment for match_previous_words().
Those restrictions exist to simplify the preprocessor.

Discussion: https://postgr.es/m/2208466.1720729502@sss.pgh.pa.us

src/bin/psql/tab-complete.c
src/tools/pgindent/typedefs.list

index 62a6c1f18d27edd1d748215f3de010f8f42cdf3e..cc3a8b7607e4151d6ab32f9b5b43d94287cc5e36 100644 (file)
@@ -4,6 +4,13 @@
  * Copyright (c) 2000-2024, PostgreSQL Global Development Group
  *
  * src/bin/psql/tab-complete.c
+ *
+ * Note: this will compile and work as-is if SWITCH_CONVERSION_APPLIED
+ * is not defined.  However, the expected usage is that it's first run
+ * through gen_tabcomplete.pl, which will #define that symbol, fill in the
+ * tcpatterns[] array, and convert the else-if chain in match_previous_words()
+ * into a switch.  See comments for match_previous_words() and the header
+ * comment in gen_tabcomplete.pl for more detail.
  */
 
 /*----------------------------------------------------------------------
@@ -1195,6 +1202,20 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
    {0, NULL}
 };
 
+ /* Known command-starting keywords. */
+static const char *const sql_commands[] = {
+   "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
+   "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
+   "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
+   "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK",
+   "MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
+   "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
+   "RESET", "REVOKE", "ROLLBACK",
+   "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
+   "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH",
+   NULL
+};
+
 /*
  * This is a list of all "things" in Pgsql, which can show up after CREATE or
  * DROP; and there is also a query to get a list of them.
@@ -1287,6 +1308,51 @@ static const pgsql_thing_t words_after_create[] = {
    {NULL}                      /* end of list */
 };
 
+/*
+ * The tcpatterns[] table provides the initial pattern-match rule for each
+ * switch case in match_previous_words().  The contents of the table
+ * are constructed by gen_tabcomplete.pl.
+ */
+
+/* Basic match rules appearing in tcpatterns[].kind */
+enum TCPatternKind
+{
+   Match,
+   MatchCS,
+   HeadMatch,
+   HeadMatchCS,
+   TailMatch,
+   TailMatchCS,
+};
+
+/* Things besides string literals that can appear in tcpatterns[].words */
+#define MatchAny  NULL
+#define MatchAnyExcept(pattern)  ("!" pattern)
+#define MatchAnyN ""
+
+/* One entry in tcpatterns[] */
+typedef struct
+{
+   int         id;             /* case label used in match_previous_words */
+   enum TCPatternKind kind;    /* match kind, see above */
+   int         nwords;         /* length of words[] array */
+   const char *const *words;   /* array of match words */
+} TCPattern;
+
+/* Macro emitted by gen_tabcomplete.pl to fill a tcpatterns[] entry */
+#define TCPAT(id, kind, ...) \
+   { (id), (kind), VA_ARGS_NARGS(__VA_ARGS__), \
+     (const char * const []) { __VA_ARGS__ } }
+
+#ifdef SWITCH_CONVERSION_APPLIED
+
+static const TCPattern tcpatterns[] =
+{
+   /* Insert tab-completion pattern data here. */
+};
+
+#endif                         /* SWITCH_CONVERSION_APPLIED */
+
 /* Storage parameters for CREATE TABLE and ALTER TABLE */
 static const char *const table_storage_parameters[] = {
    "autovacuum_analyze_scale_factor",
@@ -1340,6 +1406,10 @@ static const char *const view_optional_parameters[] = {
 
 /* Forward declaration of functions */
 static char **psql_completion(const char *text, int start, int end);
+static char **match_previous_words(int pattern_id,
+                                  const char *text, int start, int end,
+                                  char **previous_words,
+                                  int previous_words_count);
 static char *create_command_generator(const char *text, int state);
 static char *drop_command_generator(const char *text, int state);
 static char *alter_command_generator(const char *text, int state);
@@ -1449,10 +1519,6 @@ initialize_readline(void)
  * just be written directly in patterns.)  There is also MatchAnyN, but that
  * is supported only in Matches/MatchesCS and is not handled here.
  */
-#define MatchAny  NULL
-#define MatchAnyExcept(pattern)  ("!" pattern)
-#define MatchAnyN ""
-
 static bool
 word_matches(const char *pattern,
             const char *word,
@@ -1787,20 +1853,6 @@ psql_completion(const char *text, int start, int end)
    HeadMatchesImpl(true, previous_words_count, previous_words, \
                    VA_ARGS_NARGS(__VA_ARGS__), __VA_ARGS__)
 
-   /* Known command-starting keywords. */
-   static const char *const sql_commands[] = {
-       "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
-       "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
-       "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
-       "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK",
-       "MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
-       "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
-       "RESET", "REVOKE", "ROLLBACK",
-       "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
-       "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH",
-       NULL
-   };
-
    /* psql's backslash commands. */
    static const char *const backslash_commands[] = {
        "\\a",
@@ -1887,6 +1939,194 @@ psql_completion(const char *text, int start, int end)
    else if (previous_words_count == 0)
        COMPLETE_WITH_LIST(sql_commands);
 
+   /* Else try completions based on matching patterns of previous words */
+   else
+   {
+#ifdef SWITCH_CONVERSION_APPLIED
+       /*
+        * If we have transformed match_previous_words into a switch, iterate
+        * through tcpatterns[] to see which pattern ids match.
+        *
+        * For now, we have to try the patterns in the order they are stored
+        * (matching the order of switch cases in match_previous_words),
+        * because some of the logic in match_previous_words assumes that
+        * previous matches have been eliminated.  This is fairly
+        * unprincipled, and it is likely that there are undesirable as well
+        * as desirable interactions hidden in the order of the pattern
+        * checks.  TODO: think about a better way to manage that.
+        */
+       for (int tindx = 0; tindx < lengthof(tcpatterns); tindx++)
+       {
+           const TCPattern *tcpat = tcpatterns + tindx;
+           bool        match = false;
+
+           switch (tcpat->kind)
+           {
+               case Match:
+                   match = MatchesArray(false,
+                                        previous_words_count,
+                                        previous_words,
+                                        tcpat->nwords, tcpat->words);
+                   break;
+               case MatchCS:
+                   match = MatchesArray(true,
+                                        previous_words_count,
+                                        previous_words,
+                                        tcpat->nwords, tcpat->words);
+                   break;
+               case HeadMatch:
+                   match = HeadMatchesArray(false,
+                                            previous_words_count,
+                                            previous_words,
+                                            tcpat->nwords, tcpat->words);
+                   break;
+               case HeadMatchCS:
+                   match = HeadMatchesArray(true,
+                                            previous_words_count,
+                                            previous_words,
+                                            tcpat->nwords, tcpat->words);
+                   break;
+               case TailMatch:
+                   match = TailMatchesArray(false,
+                                            previous_words_count,
+                                            previous_words,
+                                            tcpat->nwords, tcpat->words);
+                   break;
+               case TailMatchCS:
+                   match = TailMatchesArray(true,
+                                            previous_words_count,
+                                            previous_words,
+                                            tcpat->nwords, tcpat->words);
+                   break;
+           }
+           if (match)
+           {
+               matches = match_previous_words(tcpat->id, text, start, end,
+                                              previous_words,
+                                              previous_words_count);
+               if (matches != NULL)
+                   break;
+           }
+       }
+#else                          /* !SWITCH_CONVERSION_APPLIED */
+       /*
+        * If gen_tabcomplete.pl hasn't been applied to this code, just let
+        * match_previous_words scan through all its patterns.
+        */
+       matches = match_previous_words(0, text, start, end,
+                                      previous_words,
+                                      previous_words_count);
+#endif                         /* SWITCH_CONVERSION_APPLIED */
+   }
+
+   /*
+    * Finally, we look through the list of "things", such as TABLE, INDEX and
+    * check if that was the previous word. If so, execute the query to get a
+    * list of them.
+    */
+   if (matches == NULL)
+   {
+       const pgsql_thing_t *wac;
+
+       for (wac = words_after_create; wac->name != NULL; wac++)
+       {
+           if (pg_strcasecmp(prev_wd, wac->name) == 0)
+           {
+               if (wac->query)
+                   COMPLETE_WITH_QUERY_LIST(wac->query,
+                                            wac->keywords);
+               else if (wac->vquery)
+                   COMPLETE_WITH_VERSIONED_QUERY_LIST(wac->vquery,
+                                                      wac->keywords);
+               else if (wac->squery)
+                   COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(wac->squery,
+                                                             wac->keywords);
+               break;
+           }
+       }
+   }
+
+   /*
+    * If we still don't have anything to match we have to fabricate some sort
+    * of default list. If we were to just return NULL, readline automatically
+    * attempts filename completion, and that's usually no good.
+    */
+   if (matches == NULL)
+   {
+       COMPLETE_WITH_CONST(true, "");
+       /* Also, prevent Readline from appending stuff to the non-match */
+       rl_completion_append_character = '\0';
+#ifdef HAVE_RL_COMPLETION_SUPPRESS_QUOTE
+       rl_completion_suppress_quote = 1;
+#endif
+   }
+
+   /* free storage */
+   free(previous_words);
+   free(words_buffer);
+   free(text_copy);
+   free(completion_ref_object);
+   completion_ref_object = NULL;
+   free(completion_ref_schema);
+   completion_ref_schema = NULL;
+
+   /* Return our Grand List O' Matches */
+   return matches;
+}
+
+/*
+ * Subroutine to try matches based on previous_words.
+ *
+ * This can operate in one of two modes.  As presented, the body of the
+ * function is a long if-else-if chain that sequentially tries each known
+ * match rule.  That works, but some C compilers have trouble with such a long
+ * else-if chain, either taking extra time to compile or failing altogether.
+ * Therefore, we prefer to transform the else-if chain into a switch, and then
+ * each call of this function considers just one match rule (under control of
+ * a loop in psql_completion()).  Compilers tend to be more ready to deal
+ * with many-arm switches than many-arm else-if chains.
+ *
+ * Each if-condition in this function must begin with a call of one of the
+ * functions Matches, HeadMatches, TailMatches, MatchesCS, HeadMatchesCS, or
+ * TailMatchesCS.  The preprocessor gen_tabcomplete.pl strips out those
+ * calls and converts them into entries in tcpatterns[], which are evaluated
+ * by the calling loop in psql_completion().  Successful matches result in
+ * calls to this function with the appropriate pattern_id, causing just the
+ * corresponding switch case to be executed.
+ *
+ * If-conditions in this function can be more complex than a single *Matches
+ * function call in one of two ways (but not both!).  They can be OR's
+ * of *Matches calls, such as
+ *  else if (Matches("ALTER", "VIEW", MatchAny, "ALTER", MatchAny) ||
+ *           Matches("ALTER", "VIEW", MatchAny, "ALTER", "COLUMN", MatchAny))
+ * or they can be a *Matches call AND'ed with some other condition, e.g.
+ *  else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) &&
+ *           !ends_with(prev_wd, ','))
+ * The former case is transformed into multiple tcpatterns[] entries and
+ * multiple case labels for the same bit of code.  The latter case is
+ * transformed into a case label and a contained if-statement.
+ *
+ * This is split out of psql_completion() primarily to separate code that
+ * gen_tabcomplete.pl should process from code that it should not, although
+ * doing so also helps to avoid extra indentation of this code.
+ *
+ * Returns a matches list, or NULL if no match.
+ */
+static char **
+match_previous_words(int pattern_id,
+                    const char *text, int start, int end,
+                    char **previous_words, int previous_words_count)
+{
+   /* This is the variable we'll return. */
+   char      **matches = NULL;
+
+   /* Dummy statement, allowing all the match rules to look like "else if" */
+   if (0)
+        /* skip */ ;
+
+   /* gen_tabcomplete.pl begins special processing here */
+   /* BEGIN GEN_TABCOMPLETE */
+
 /* CREATE */
    /* complete with something you can create */
    else if (TailMatches("CREATE"))
@@ -1985,9 +2225,10 @@ psql_completion(const char *text, int start, int end)
    /* ALTER PUBLICATION <name> ADD */
    else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD"))
        COMPLETE_WITH("TABLES IN SCHEMA", "TABLE");
-   else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
-            (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
-             ends_with(prev_wd, ',')))
+   else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
+       COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+   else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
+            ends_with(prev_wd, ','))
        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
    /*
@@ -2452,8 +2693,7 @@ psql_completion(const char *text, int start, int end)
    }
    /* ALTER TABLE xxx ADD [COLUMN] yyy */
    else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "COLUMN", MatchAny) ||
-            (Matches("ALTER", "TABLE", MatchAny, "ADD", MatchAny) &&
-             !Matches("ALTER", "TABLE", MatchAny, "ADD", "COLUMN|CONSTRAINT|CHECK|UNIQUE|PRIMARY|EXCLUDE|FOREIGN")))
+            Matches("ALTER", "TABLE", MatchAny, "ADD", MatchAnyExcept("COLUMN|CONSTRAINT|CHECK|UNIQUE|PRIMARY|EXCLUDE|FOREIGN")))
        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
    /* ALTER TABLE xxx ADD CONSTRAINT yyy */
    else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
@@ -3835,13 +4075,14 @@ psql_completion(const char *text, int start, int end)
                     "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
                     MatchAny) ||
             Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
-            (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
-             ends_with(prev_wd, ')')) ||
             Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
             Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
             Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
             Matches("DROP", "TEXT", "SEARCH", "CONFIGURATION|DICTIONARY|PARSER|TEMPLATE", MatchAny))
        COMPLETE_WITH("CASCADE", "RESTRICT");
+   else if (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
+            ends_with(prev_wd, ')'))
+       COMPLETE_WITH("CASCADE", "RESTRICT");
 
    /* help completing some of the variants */
    else if (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
@@ -5120,58 +5361,9 @@ psql_completion(const char *text, int start, int end)
        matches = rl_completion_matches(text, complete_from_files);
    }
 
-   /*
-    * Finally, we look through the list of "things", such as TABLE, INDEX and
-    * check if that was the previous word. If so, execute the query to get a
-    * list of them.
-    */
-   else
-   {
-       const pgsql_thing_t *wac;
-
-       for (wac = words_after_create; wac->name != NULL; wac++)
-       {
-           if (pg_strcasecmp(prev_wd, wac->name) == 0)
-           {
-               if (wac->query)
-                   COMPLETE_WITH_QUERY_LIST(wac->query,
-                                            wac->keywords);
-               else if (wac->vquery)
-                   COMPLETE_WITH_VERSIONED_QUERY_LIST(wac->vquery,
-                                                      wac->keywords);
-               else if (wac->squery)
-                   COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(wac->squery,
-                                                             wac->keywords);
-               break;
-           }
-       }
-   }
-
-   /*
-    * If we still don't have anything to match we have to fabricate some sort
-    * of default list. If we were to just return NULL, readline automatically
-    * attempts filename completion, and that's usually no good.
-    */
-   if (matches == NULL)
-   {
-       COMPLETE_WITH_CONST(true, "");
-       /* Also, prevent Readline from appending stuff to the non-match */
-       rl_completion_append_character = '\0';
-#ifdef HAVE_RL_COMPLETION_SUPPRESS_QUOTE
-       rl_completion_suppress_quote = 1;
-#endif
-   }
-
-   /* free storage */
-   free(previous_words);
-   free(words_buffer);
-   free(text_copy);
-   free(completion_ref_object);
-   completion_ref_object = NULL;
-   free(completion_ref_schema);
-   completion_ref_schema = NULL;
+   /* gen_tabcomplete.pl ends special processing here */
+   /* END GEN_TABCOMPLETE */
 
-   /* Return our Grand List O' Matches */
    return matches;
 }
 
index c4de597b1fa8c03d58e3c7ae432482abe6ad6baf..a65e1c07c5d33478aa5ab1a979f62eb7e9b23faa 100644 (file)
@@ -2810,6 +2810,7 @@ TBMSharedIterator
 TBMSharedIteratorState
 TBMStatus
 TBlockState
+TCPattern
 TIDBitmap
 TM_FailureData
 TM_IndexDelete