From 121213d9d8527f880f153e4a032ee1a4cd43833f Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 21 Sep 2018 15:22:26 -0400 Subject: [PATCH] Improve tab completion for ANALYZE, EXPLAIN, and VACUUM. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we made no attempt to provide tab completion in these statements' optional parenthesized options lists. This patch teaches psql to do so. To prevent the option completions from being offered after we've already seen a complete parenthesized option list, it's necessary to improve word_matches() so that it allows a wildcard '*' in the middle of an alternative, not only at the end as formerly. That requires only a little more code than before, and it allows us to test for "incomplete parenthesized options" with a test like else if (HeadMatches2("EXPLAIN", "(*") && !HeadMatches2("EXPLAIN", "(*)")) In addition, add some logic to offer column names in the context of "ANALYZE tablename ( ...", and likewise for VACUUM. This isn't real complete; it won't offer column names again after a comma. But it's better than before, and it doesn't take much code. Justin Pryzby, reviewed at various times by Álvaro Herrera, Arthur Zakirov, and Edmund Horner; some additional fixups by me Discussion: https://postgr.es/m/20180529000623.GA21896@telsasoft.com --- src/bin/psql/tab-complete.c | 129 ++++++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index d3fd0feb48..a486348717 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1131,9 +1131,8 @@ initialize_readline(void) * If pattern is NULL, it's a wild card that matches any word. * If pattern begins with '!', the result is negated, ie we check that 'word' * does *not* match any alternative appearing in the rest of 'pattern'. - * Any alternative can end with '*' which is a wild card, i.e., it means - * match any word that matches the characters so far. (We do not currently - * support '*' elsewhere than the end of an alternative.) + * Any alternative can contain '*' which is a wild card, i.e., it can match + * any substring; however, we allow at most one '*' per alternative. * * For readability, callers should use the macros MatchAny and MatchAnyExcept * to invoke those two special cases for 'pattern'. (But '|' and '*' must @@ -1147,8 +1146,10 @@ word_matches_internal(const char *pattern, const char *word, bool case_sensitive) { - size_t wordlen, - patternlen; + size_t wordlen; + +#define cimatch(s1, s2, n) \ + (case_sensitive ? strncmp(s1, s2, n) == 0 : pg_strncasecmp(s1, s2, n) == 0) /* NULL pattern matches anything. */ if (pattern == NULL) @@ -1162,31 +1163,34 @@ word_matches_internal(const char *pattern, wordlen = strlen(word); for (;;) { + const char *star = NULL; const char *c; - /* Find end of current alternative. */ + /* Find end of current alternative, and locate any wild card. */ c = pattern; while (*c != '\0' && *c != '|') + { + if (*c == '*') + star = c; c++; - /* Was there a wild card? (Assumes first alternative is not empty) */ - if (c[-1] == '*') + } + /* Was there a wild card? */ + if (star) { /* Yes, wildcard match? */ - patternlen = c - pattern - 1; - if (wordlen >= patternlen && - (case_sensitive ? - strncmp(word, pattern, patternlen) == 0 : - pg_strncasecmp(word, pattern, patternlen) == 0)) + size_t beforelen = star - pattern, + afterlen = c - star - 1; + + if (wordlen >= (beforelen + afterlen) && + cimatch(word, pattern, beforelen) && + cimatch(word + wordlen - afterlen, star + 1, afterlen)) return true; } else { /* No, plain match? */ - patternlen = c - pattern; - if (wordlen == patternlen && - (case_sensitive ? - strncmp(word, pattern, wordlen) == 0 : - pg_strncasecmp(word, pattern, wordlen) == 0)) + if (wordlen == (c - pattern) && + cimatch(word, pattern, wordlen)) return true; } /* Out of alternatives? */ @@ -2158,6 +2162,24 @@ psql_completion(const char *text, int start, int end) else if (Matches5("ALTER", "TYPE", MatchAny, "RENAME", "VALUE")) COMPLETE_WITH_ENUM_VALUE(prev3_wd); +/* + * ANALYZE [ ( option [, ...] ) ] [ table_and_columns [, ...] ] + * ANALYZE [ VERBOSE ] [ table_and_columns [, ...] ] + * + * Currently the only allowed option is VERBOSE, so we can be skimpier on + * the option processing than VACUUM has to be. + */ + else if (Matches1("ANALYZE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, + " UNION SELECT 'VERBOSE'"); + else if (Matches2("ANALYZE", "(")) + COMPLETE_WITH_CONST("VERBOSE)"); + else if (HeadMatches1("ANALYZE") && TailMatches1("(")) + /* "ANALYZE (" should be caught above, so assume we want columns */ + COMPLETE_WITH_ATTR(prev2_wd, ""); + else if (HeadMatches1("ANALYZE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, NULL); + /* BEGIN */ else if (Matches1("BEGIN")) COMPLETE_WITH_LIST6("WORK", "TRANSACTION", "ISOLATION LEVEL", "READ", "DEFERRABLE", "NOT DEFERRABLE"); @@ -2817,18 +2839,34 @@ psql_completion(const char *text, int start, int end) else if (Matches1("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); -/* EXPLAIN */ - - /* - * Complete EXPLAIN [ANALYZE] [VERBOSE] with list of EXPLAIN-able commands - */ +/* + * EXPLAIN [ ( option [, ...] ) ] statement + * EXPLAIN [ ANALYZE ] [ VERBOSE ] statement + */ else if (Matches1("EXPLAIN")) COMPLETE_WITH_LIST7("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", "ANALYZE", "VERBOSE"); + else if (HeadMatches2("EXPLAIN", "(*") && + !HeadMatches2("EXPLAIN", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH_LIST7("ANALYZE", "VERBOSE", "COSTS", "BUFFERS", + "TIMING", "SUMMARY", "FORMAT"); + else if (TailMatches1("ANALYZE|VERBOSE|COSTS|BUFFERS|TIMING|SUMMARY")) + COMPLETE_WITH_LIST2("ON", "OFF"); + else if (TailMatches1("FORMAT")) + COMPLETE_WITH_LIST4("TEXT", "XML", "JSON", "YAML"); + } else if (Matches2("EXPLAIN", "ANALYZE")) COMPLETE_WITH_LIST6("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", "VERBOSE"); - else if (Matches2("EXPLAIN", "VERBOSE") || + else if (Matches2("EXPLAIN", "(*)") || + Matches2("EXPLAIN", "VERBOSE") || Matches3("EXPLAIN", "ANALYZE", "VERBOSE")) COMPLETE_WITH_LIST5("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE"); @@ -3383,8 +3421,8 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_CONST("OPTIONS"); /* - * VACUUM [ FULL | FREEZE ] [ VERBOSE ] [ table ] - * VACUUM [ FULL | FREEZE ] [ VERBOSE ] ANALYZE [ table [ (column [, ...] ) ] ] + * VACUUM [ ( option [, ...] ) ] [ table_and_columns [, ...] ] + * VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ] */ else if (Matches1("VACUUM")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, @@ -3392,22 +3430,36 @@ psql_completion(const char *text, int start, int end) " UNION SELECT 'FREEZE'" " UNION SELECT 'ANALYZE'" " UNION SELECT 'VERBOSE'"); - else if (Matches2("VACUUM", "FULL|FREEZE")) + else if (Matches2("VACUUM", "FULL")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, + " UNION SELECT 'FREEZE'" " UNION SELECT 'ANALYZE'" " UNION SELECT 'VERBOSE'"); - else if (Matches3("VACUUM", "FULL|FREEZE", "ANALYZE")) + else if (Matches2("VACUUM", "FREEZE") || + Matches3("VACUUM", "FULL", "FREEZE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, - " UNION SELECT 'VERBOSE'"); - else if (Matches3("VACUUM", "FULL|FREEZE", "VERBOSE")) + " UNION SELECT 'VERBOSE'" + " UNION SELECT 'ANALYZE'"); + else if (Matches2("VACUUM", "VERBOSE") || + Matches3("VACUUM", "FULL|FREEZE", "VERBOSE") || + Matches4("VACUUM", "FULL", "FREEZE", "VERBOSE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, " UNION SELECT 'ANALYZE'"); - else if (Matches2("VACUUM", "VERBOSE")) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, - " UNION SELECT 'ANALYZE'"); - else if (Matches2("VACUUM", "ANALYZE")) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, - " UNION SELECT 'VERBOSE'"); + else if (HeadMatches2("VACUUM", "(*") && + !HeadMatches2("VACUUM", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH_LIST5("FULL", "FREEZE", "ANALYZE", "VERBOSE", + "DISABLE_PAGE_SKIPPING"); + } + else if (HeadMatches1("VACUUM") && TailMatches1("(")) + /* "VACUUM (" should be caught above, so assume we want columns */ + COMPLETE_WITH_ATTR(prev2_wd, ""); else if (HeadMatches1("VACUUM")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, NULL); @@ -3420,11 +3472,6 @@ psql_completion(const char *text, int start, int end) else if (Matches1("WITH")) COMPLETE_WITH_CONST("RECURSIVE"); -/* ANALYZE */ - /* Complete with list of appropriate relations */ - else if (Matches1("ANALYZE")) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, NULL); - /* WHERE */ /* Simple case of the word before the where being the table name */ else if (TailMatches2(MatchAny, "WHERE"))