From bd1276a3c9433a9e2760db6ae6e73560d7d32a22 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 7 Oct 2024 12:19:12 -0400 Subject: [PATCH] Prepare tab-complete.c for preprocessing. 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 | 344 ++++++++++++++++++++++++------- src/tools/pgindent/typedefs.list | 1 + 2 files changed, 269 insertions(+), 76 deletions(-) diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 62a6c1f18d2..cc3a8b7607e 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -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 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; } diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c4de597b1fa..a65e1c07c5d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2810,6 +2810,7 @@ TBMSharedIterator TBMSharedIteratorState TBMStatus TBlockState +TCPattern TIDBitmap TM_FailureData TM_IndexDelete -- 2.30.2