Improve tab completion for ANALYZE, EXPLAIN, and VACUUM.

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
This commit is contained in:
Tom Lane 2018-09-21 15:22:26 -04:00
parent e3b7a6d165
commit 121213d9d8
1 changed files with 88 additions and 41 deletions

View File

@ -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"))