Обсуждение: Support tab completion for upper character inputs in psql

Поиск
Список
Период
Сортировка

Support tab completion for upper character inputs in psql

От
"Tang, Haiying"
Дата:
Hi Hackers,

When using psql I found there's no tab completion for upper character inputs. It's really inconvenient sometimes so I
tryto fix this problem in the attached patch. 

Here is the examples to show what this patch can do.
Action:
1. connect the db using psql
2. input SQL command
3. enter TAB key(twice at the very first time)

Results:
[master]
postgres=# set a
all                      allow_system_table_mods  application_name         array_nulls
postgres=# set A

postgres=# set A

[patched]
postgres=# set a
all                      allow_system_table_mods  application_name         array_nulls
postgres=# set A
ALL                      ALLOW_SYSTEM_TABLE_MODS  APPLICATION_NAME         ARRAY_NULLS
postgres=# set A

Please take a check at this patch. Any comment is welcome.

Regards,
Tang



Вложения

Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
"Tang, Haiying" <tanghy.fnst@cn.fujitsu.com> writes:
> When using psql I found there's no tab completion for upper character inputs. It's really inconvenient sometimes so I
tryto fix this problem in the attached patch. 

This looks like you're trying to force case-insensitive behavior
whether that is appropriate or not.  Does not sound like a good
idea.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Kyotaro Horiguchi
Дата:
At Sun, 07 Feb 2021 13:55:00 -0500, Tom Lane <tgl@sss.pgh.pa.us> wrote in 
> "Tang, Haiying" <tanghy.fnst@cn.fujitsu.com> writes:
> > When using psql I found there's no tab completion for upper character inputs. It's really inconvenient sometimes so
Itry to fix this problem in the attached patch.
 
> 
> This looks like you're trying to force case-insensitive behavior
> whether that is appropriate or not.  Does not sound like a good
> idea.

Agreed. However I'm not sure what the OP exactly wants, \set behaves
in a different but similar way.

=# \set c[tab]
=# \set COMP_KEYWORD_CASE _

However set doesn't. If it is what is wanted, the following change on
Query_for_list_of_set_vars works (only for the case of SET/RESET
commands).


diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5f0e775fd3..5c2a263785 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -725,7 +725,8 @@ static const SchemaQuery Query_for_list_of_statistics = {
 "  UNION ALL SELECT 'role' "\
 "  UNION ALL SELECT 'tablespace' "\
 "  UNION ALL SELECT 'all') ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE substring(name,1,%1$d)='%2$s' "\
+"    OR pg_catalog.lower(substring(name,1,%1$d))=pg_catalog.lower('%2$s')"
 
 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\

=# set AP[tab]
=# set application_name _

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center



RE: Support tab completion for upper character inputs in psql

От
"Tang, Haiying"
Дата:
At Sun, 07 Feb 2021 13:55:00 -0500, Tom Lane <tgl@sss.pgh.pa.us> wrote in
>
> This looks like you're trying to force case-insensitive behavior
> whether that is appropriate or not.  Does not sound like a good idea.

Thanks for your reply.
I raise this issue because I thought all SQL command should be case-insensitive.
And the set/reset/show commands work well no matter the input configuration parameter is in upper or in lower case.
My modification is not good enough, but I really think it's more convenient if we can support the tab-completion for
uppercharacter inputs. 

=# set APPLICATION_NAME to test;
SET

=# show APPLICATION_name;
 application_name
------------------
 test
(1 row)

From: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Sent: Monday, February 8, 2021 5:02 PM

>However set doesn't. If it is what is wanted, the following change on Query_for_list_of_set_vars works (only for the
caseof SET/RESET commands). 

Thanks for your update. I applied your patch, it works well for SET/RESET commands.
I added the same modification to SHOW command. The new patch(V2) can support tab completion for upper character inputs
inpsql for SET/RESET/SHOW commands. 

Regards,
Tang



Вложения

RE: Support tab completion for upper character inputs in psql

От
"Tang, Haiying"
Дата:
At Sun, 07 Feb 2021 13:55:00 -0500, Tom Lane <tgl@sss.pgh.pa.us> wrote in
>
> This looks like you're trying to force case-insensitive behavior
> whether that is appropriate or not.  Does not sound like a good idea.

I'm still confused about the APPROPRIATE behavior of tab completion.
It seems ALTER table/tablespace <name> SET/RESET is already case-insensitive.

For example
# alter tablespace dbspace set(e[tab]
# alter tablespace dbspace set(effective_io_concurrency

# alter tablespace dbspace set(E[tab]
# alter tablespace dbspace set(EFFECTIVE_IO_CONCURRENCY

The above behavior is exactly the same as what the patch(attached in the following message) did for SET/RESET etc.
https://www.postgresql.org/message-id/flat/a63cbd45e3884cf9b3961c2a6a95dcb7%40G08CNEXMBPEKD05.g08.fujitsu.local

If anyone can share me some cases which show inappropriate scenarios of forcing case-insensitive inputs in psql.
I'd be grateful for that.

Regards,
Tang






Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 09.02.21 15:48, Tang, Haiying wrote:
> I'm still confused about the APPROPRIATE behavior of tab completion.
> It seems ALTER table/tablespace <name> SET/RESET is already case-insensitive.
> 
> For example
> # alter tablespace dbspace set(e[tab]
> # alter tablespace dbspace set(effective_io_concurrency
> 
> # alter tablespace dbspace set(E[tab]
> # alter tablespace dbspace set(EFFECTIVE_IO_CONCURRENCY

This case completes with a hardcoded list, which is done  
case-insensitively by default.  The cases that complete with a query  
result are not case insensitive right now.  This affects things like

UPDATE T<tab>

as well.  I think your first patch was basically right.  But we need to  
understand that this affects all completions with query results, not  
just the one you wanted to fix.  So you should analyze all the callers  
and explain why the proposed change is appropriate.



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Tuesday, March 16, 2021 5:20 AM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:

>The cases that complete with a query
>result are not case insensitive right now.  This affects things like
>
>UPDATE T<tab>
>
>as well.  I think your first patch was basically right.  But we need to
>understand that this affects all completions with query results, not
>just the one you wanted to fix.  So you should analyze all the callers
>and explain why the proposed change is appropriate.

Thanks for your review and suggestion. Please find attached patch V3 which was based on the first patch[1].
Difference from the first patch is:

Add tab completion support for all query results in psql.
complete_from_query
+complete_from_versioned_query
+complete_from_schema_query
+complete_from_versioned_schema_query

[1] https://www.postgresql.org/message-id/a63cbd45e3884cf9b3961c2a6a95dcb7%40G08CNEXMBPEKD05.g08.fujitsu.local

The modification to support case insensitive matching in " _complete_from_query" is based on "complete_from_const and
"complete_from_list". 
Please let me know if you find anything insufficient.

Regards,
Tang



Вложения

Re: Support tab completion for upper character inputs in psql

От
David Zhang
Дата:
Hi Tang,

Thanks a lot for the patch.

I did a quick test based on the latest patch V3 on latest master branch 
"commit 4753ef37e0eda4ba0af614022d18fcbc5a946cc9".

Case 1: before patch

   1 postgres=# set a
   2 all                      allow_system_table_mods 
application_name         array_nulls
   3 postgres=# set A
   4
   5 postgres=# create TABLE tbl (data text);
   6 CREATE TABLE
   7 postgres=# update tbl SET DATA =
   8
   9 postgres=# update T
  10
  11 postgres=#

Case 2: after patched

   1 postgres=# set a
   2 all                      allow_system_table_mods 
application_name         array_nulls
   3 postgres=# set A
   4 ALL                      ALLOW_SYSTEM_TABLE_MODS 
APPLICATION_NAME         ARRAY_NULLS
   5 postgres=# create TABLE tbl (data text);
   6 CREATE TABLE
   7
   8 postgres=# update tbl SET DATA =
   9
  10 postgres=# update TBL SET
  11
  12 postgres=#

So, as you can see the difference is between line 8 and 10 in case 2. It 
looks like the lowercase can auto complete more than the uppercase; 
secondly, if you can add some test cases, it would be great.

Best regards,
David

On 2021-03-22 5:41 a.m., tanghy.fnst@fujitsu.com wrote:
> On Tuesday, March 16, 2021 5:20 AM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:
>
>> The cases that complete with a query
>> result are not case insensitive right now.  This affects things like
>>
>> UPDATE T<tab>
>>
>> as well.  I think your first patch was basically right.  But we need to
>> understand that this affects all completions with query results, not
>> just the one you wanted to fix.  So you should analyze all the callers
>> and explain why the proposed change is appropriate.
> Thanks for your review and suggestion. Please find attached patch V3 which was based on the first patch[1].
> Difference from the first patch is:
>
> Add tab completion support for all query results in psql.
> complete_from_query
> +complete_from_versioned_query
> +complete_from_schema_query
> +complete_from_versioned_schema_query
>
> [1] https://www.postgresql.org/message-id/a63cbd45e3884cf9b3961c2a6a95dcb7%40G08CNEXMBPEKD05.g08.fujitsu.local
>
> The modification to support case insensitive matching in " _complete_from_query" is based on "complete_from_const and
"complete_from_list".
 
> Please let me know if you find anything insufficient.
>
> Regards,
> Tang
>
>
-- 
David

Software Engineer
Highgo Software Inc. (Canada)
www.highgo.ca



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Wednesday, March 31, 2021 4:05 AM, David Zhang <david.zhang@highgo.ca> wrote

>   8 postgres=# update tbl SET DATA =
>   9
>  10 postgres=# update TBL SET
>  11
>  12 postgres=#
>
>So, as you can see the difference is between line 8 and 10 in case 2. It 
>looks like the lowercase can auto complete more than the uppercase; 
>secondly, if you can add some test cases, it would be great.

Thanks for your test. I fix the bug and add some tests for it.
Please find attached the latest patch V4.

Differences from v3 are:
* fix an issue reported by Zhang [1] where a scenario was found which still wasn't able to realize tap completion in
query.
* add some tap tests.

[1] https://www.postgresql.org/message-id/3140db2a-9808-c470-7e60-de39c431b3ab%40highgo.ca

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 01.04.21 11:40, tanghy.fnst@fujitsu.com wrote:
> On Wednesday, March 31, 2021 4:05 AM, David Zhang <david.zhang@highgo.ca> wrote
> 
>>    8 postgres=# update tbl SET DATA =
>>    9
>>   10 postgres=# update TBL SET
>>   11
>>   12 postgres=#
>>
>> So, as you can see the difference is between line 8 and 10 in case 2. It
>> looks like the lowercase can auto complete more than the uppercase;
>> secondly, if you can add some test cases, it would be great.
> 
> Thanks for your test. I fix the bug and add some tests for it.
> Please find attached the latest patch V4.
> 
> Differences from v3 are:
> * fix an issue reported by Zhang [1] where a scenario was found which still wasn't able to realize tap completion in
query.
> * add some tap tests.

Seeing the tests you provided, it's pretty obvious that the current 
behavior is insufficient.  I think we could probably think of a few more 
tests, for example exercising the "If case insensitive matching was 
requested initially, adjust the case according to setting." case, or 
something with quoted identifiers.  I'll push this to the next commit 
fest for now.  I encourage you to keep working on it.



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Thursday, April 8, 2021 4:14 PM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote

>Seeing the tests you provided, it's pretty obvious that the current 
>behavior is insufficient.  I think we could probably think of a few more 
>tests, for example exercising the "If case insensitive matching was 
>requested initially, adjust the case according to setting." case, or 
>something with quoted identifiers.

Thanks for your review and suggestions on my patch. 
I've added more tests in the latest patch V5, the added tests helped me find some bugs in my patch and I fixed them.
Now the patch can support not only the SET/SHOW [PARAMETER] but also UPDATE ["aTable"|ATABLE], also UPDATE atable SET
["aColumn"|ACOLUMN].

I really hope someone can have more tests suggestions on my patch or kindly do some tests on my patch and share me if
anybugs happened.
 

Differences from V4 are:
* fix some bugs related to quoted identifiers.
* add some tap tests.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Smith
Дата:
On Wed, Apr 14, 2021 at 11:34 PM tanghy.fnst@fujitsu.com
<tanghy.fnst@fujitsu.com> wrote:
>
> On Thursday, April 8, 2021 4:14 PM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote
>
> >Seeing the tests you provided, it's pretty obvious that the current
> >behavior is insufficient.  I think we could probably think of a few more
> >tests, for example exercising the "If case insensitive matching was
> >requested initially, adjust the case according to setting." case, or
> >something with quoted identifiers.
>
> Thanks for your review and suggestions on my patch.
> I've added more tests in the latest patch V5, the added tests helped me find some bugs in my patch and I fixed them.
> Now the patch can support not only the SET/SHOW [PARAMETER] but also UPDATE ["aTable"|ATABLE], also UPDATE atable SET
["aColumn"|ACOLUMN].
>
> I really hope someone can have more tests suggestions on my patch or kindly do some tests on my patch and share me if
anybugs happened.
 
>
> Differences from V4 are:
> * fix some bugs related to quoted identifiers.
> * add some tap tests.

I tried playing a bit with your psql patch V5 and I did not find any
problems - it seemed to work as advertised.

Below are a few code review comments.

====

1. Patch applies with whitespace warnings.

[postgres@CentOS7-x64 oss_postgres_2PC]$ git apply
../patches_misc/V5-0001-Support-tab-completion-with-a-query-result-for-upper.patch
../patches_misc/V5-0001-Support-tab-completion-with-a-query-result-for-upper.patch:130:
trailing whitespace.
}
warning: 1 line adds whitespace errors.

====

2. Unrelated "code tidy" fixes maybe should be another patch?

I noticed there are a couple of "code tidy" fixes combined with this
patch - e.g. passing fixes to some code comments and blank lines etc
(see below). Although they are all good improvements, they maybe don't
really have anything to do with your feature/bugfix so I am not sure
if they should be included here. Maybe post a separate patch for these
ones?

@@ -1028,7 +1032,7 @@ static const VersionedQuery
Query_for_list_of_subscriptions[] = {
 };

 /*
- * This is a list of all "things" in Pgsql, which can show up after CREATE or
+ * 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.
  */

@@ -4607,7 +4642,6 @@ complete_from_list(const char *text, int state)
  if (completion_case_sensitive)
  return pg_strdup(item);
  else
-
  /*
  * If case insensitive matching was requested initially,
  * adjust the case according to setting.
@@ -4660,7 +4694,6 @@ complete_from_const(const char *text, int state)
  if (completion_case_sensitive)
  return pg_strdup(completion_charp);
  else
-
  /*
  * If case insensitive matching was requested initially, adjust
  * the case according to setting.

====

3. Unnecessary NULL check?

@@ -4420,16 +4425,37 @@ _complete_from_query(const char *simple_query,
  PQclear(result);
  result = NULL;

- /* Set up suitably-escaped copies of textual inputs */
+ /* Set up suitably-escaped copies of textual inputs,
+ * then change the textual inputs to lower case.
+ */
  e_text = escape_string(text);
+ if(e_text != NULL)
+ {
+ if(e_text[0] == '"')
+ completion_case_sensitive = true;
+ else
+ e_text = pg_string_tolower(e_text);
+ }

Perhaps that check "if(e_text != NULL)" is unnecessary. That function
hardly looks capable of returning a NULL, and other callers are not
checking the return like this.

====

4. Memory not freed in multiple places?

@@ -4420,16 +4425,37 @@ _complete_from_query(const char *simple_query,
  PQclear(result);
  result = NULL;

- /* Set up suitably-escaped copies of textual inputs */
+ /* Set up suitably-escaped copies of textual inputs,
+ * then change the textual inputs to lower case.
+ */
  e_text = escape_string(text);
+ if(e_text != NULL)
+ {
+ if(e_text[0] == '"')
+ completion_case_sensitive = true;
+ else
+ e_text = pg_string_tolower(e_text);
+ }

  if (completion_info_charp)
+ {
  e_info_charp = escape_string(completion_info_charp);
+ if(e_info_charp[0] == '"')
+ completion_case_sensitive = true;
+ else
+ e_info_charp = pg_string_tolower(e_info_charp);
+ }
  else
  e_info_charp = NULL;

  if (completion_info_charp2)
+ {
  e_info_charp2 = escape_string(completion_info_charp2);
+ if(e_info_charp2[0] == '"')
+ completion_case_sensitive = true;
+ else
+ e_info_charp2 = pg_string_tolower(e_info_charp2);
+ }
  else
  e_info_charp2 = NULL;

The function escape_string has a comment saying "The returned value
has to be freed." but in the above code you are overwriting the
escape_string result with the strdup'ed pg_string_tolower but without
free-ing the original e_text/e_info_charp/e_info_charp2.

======

5. strncmp replacement?

@@ -4464,7 +4490,7 @@ _complete_from_query(const char *simple_query,
  */
  if (strcmp(schema_query->catname,
     "pg_catalog.pg_class c") == 0 &&
- strncmp(text, "pg_", 3) != 0)
+ strncmp(pg_string_tolower(text), "pg_", 3) != 0)
  {
  appendPQExpBufferStr(&query_buffer,
  " AND c.relnamespace <> (SELECT oid FROM"

Why not use strnicmp for case insensitive compare here instead of
strdup'ing another string (and not freeing it)?

Or maybe use pg_strncasecmp.

======

6. byte_length == 0?

@@ -4556,7 +4582,16 @@ _complete_from_query(const char *simple_query,
  while (list_index < PQntuples(result) &&
     (item = PQgetvalue(result, list_index++, 0)))
  if (pg_strncasecmp(text, item, byte_length) == 0)
- return pg_strdup(item);
+ {
+ if (byte_length == 0 || completion_case_sensitive)
+ return pg_strdup(item);
+ else
+ /*
+ * If case insensitive matching was requested initially,
+ * adjust the case according to setting.
+ */
+ return pg_strdup_keyword_case(item, text);
+ }
  }
The byte_length was not being checked before, so why is the check needed now?

======

7. test typo "ralation"

+# check query command completion for upper character ralation name
+check_completion("update TAB1 SET \t", qr/update TAB1 SET \af/,
"complete column name for TAB1");

======

8. test typo "case-insensitiveq"

+# check schema query(upper case) which is case-insensitiveq
+check_completion("select oid from Pg_cla\t", qq/select oid from
Pg_cla\b\b\b\b\bG_CLASS /, "complete schema query with uppper case
string");

------
Kind Regards,
Peter Smith.
Fujitsu Australia



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Wednesday, April 21, 2021 1:24 PM, Peter Smith <smithpb2250@gmail.com> Wrote

>I tried playing a bit with your psql patch V5 and I did not find any
>problems - it seemed to work as advertised.
>
>Below are a few code review comments.

Thanks for you review. I've updated the patch to V6 according to your comments.

>1. Patch applies with whitespace warnings.
Fixed.

>2. Unrelated "code tidy" fixes maybe should be another patch?
Agreed. Will post this modification on another thread.

>3. Unnecessary NULL check?
Agreed. NULL check removed.

>4. Memory not freed in multiple places?
oops. Memory free added.

>5. strncmp replacement?
Agreed. Thanks for your advice. Since this modification has little relation with my patch here.
I will merge this with comment(2) and push this on another patch.

>6. byte_length == 0?
>The byte_length was not being checked before, so why is the check needed now?

We need to make sure the empty input to be case sensitive as before(HEAD).
For example
    CREATE TABLE onetab1 (f1 int);
    update onetab1 SET [tab]

Without the check of "byte_length == 0", pg_strdup_keyword_case will make the column name "f1" to be upper case "F1".
Namely, the output will be " update onetab1 SET F1" which is not so good.

I added some tab tests for this empty input case, too. 

>7. test typo "ralation"
>8. test typo "case-insensitiveq"
Thanks, typo fixed. 

Any further comment is very welcome.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Kyotaro Horiguchi
Дата:
At Thu, 22 Apr 2021 12:43:42 +0000, "tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> wrote in 
> On Wednesday, April 21, 2021 1:24 PM, Peter Smith <smithpb2250@gmail.com> Wrot> >4. Memory not freed in multiple
places?
> oops. Memory free added.

All usages of pg_string_tolower don't need a copy.
So don't we change the function to in-place converter?

> >6. byte_length == 0?
> >The byte_length was not being checked before, so why is the check needed now?
> 
> We need to make sure the empty input to be case sensitive as before(HEAD).
> For example
>     CREATE TABLE onetab1 (f1 int);
>     update onetab1 SET [tab]
> 
> Without the check of "byte_length == 0", pg_strdup_keyword_case will make the column name "f1" to be upper case
"F1".
> Namely, the output will be " update onetab1 SET F1" which is not so good.
> 
> I added some tab tests for this empty input case, too. 
> 
> >7. test typo "ralation"
> >8. test typo "case-insensitiveq"
> Thanks, typo fixed. 
> 
> Any further comment is very welcome.

         if (completion_info_charp)
+        {
             e_info_charp = escape_string(completion_info_charp);
+            if(e_info_charp[0] == '"')
+                completion_case_sensitive = true;
+            else
+            {
+                le_str = pg_string_tolower(e_info_charp);

It seems right to lower completion_info_charp and ..2 but it is not
right that change completion_case_sensitive here, which only affects
the returned candidates.  This change prevents the following operation
from getting the expected completion candidates.

=# create table "T" (a int) partition by range(a);
=# create table c1 partition of "T" for values from (0) to (10);
=# alter table "T" drop partition C<tab>

Is there any reason for doing that?



+                if (byte_length == 0 || completion_case_sensitive)

Is the condition "byte_length == 0 ||" right?

This results in a maybe-unexpected behavior,

=# \set COM_KEYWORD_CASE upper
=# create table t (a int) partition by range(a);
=# create table d1 partition of t for values from (0) to (10);
=# alter table t drop partition <tab>

This results in 

=# alter table t drop partition d1

I think we are expecting D1 as the result.

By the way COMP_KEYWORD_CASE suggests that *keywords* are completed
following the setting. However, they are not keywords, but
identifiers. And some people (including me) might dislike that
keywords and identifiers follow the same setting.  Specifically I
sometimes want keywords to be upper-cased but identifiers (always) be
lower-cased.


regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
Kyotaro Horiguchi <horikyota.ntt@gmail.com> writes:
> All usages of pg_string_tolower don't need a copy.
> So don't we change the function to in-place converter?

Doesn't seem like a good idea, because that locks us into an assumption
that the downcasing conversion doesn't change the string's physical
length.  There are a lot of counterexamples to that :-(.  I'm not sure
that we actually implement such cases correctly today, but let's not
build APIs that prevent it from being fixed.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Kyotaro Horiguchi
Дата:
At Fri, 23 Apr 2021 11:58:12 +0900 (JST), Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote in 
> > Any further comment is very welcome.

Oh, I accidentally found a doubious behsbior.

=# alter table public.<tab>
public.c1    public.d1    public."t"   public.t     public."tt"  

The "t" and "tt" are needlessly lower-cased.

# \d
                     List of relations
 Schema |        Name        |       Type        |  Owner   
--------+--------------------+-------------------+----------
 public | T                  | partitioned table | horiguti
 public | TT                 | table             | horiguti
 public | c1                 | table             | horiguti
 public | d1                 | table             | horiguti
 public | t                  | partitioned table | horiguti

=# alter table public."<tab>
=# alter table public."t        -- candidates are "t" and "tt"?
=# alter table public."tt<tab>  -- nothing happenes
=# alter table public."TT<tab>  -- also nothing happenes

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center



Re: Support tab completion for upper character inputs in psql

От
Kyotaro Horiguchi
Дата:
At Thu, 22 Apr 2021 23:17:19 -0400, Tom Lane <tgl@sss.pgh.pa.us> wrote in 
> Kyotaro Horiguchi <horikyota.ntt@gmail.com> writes:
> > All usages of pg_string_tolower don't need a copy.
> > So don't we change the function to in-place converter?
> 
> Doesn't seem like a good idea, because that locks us into an assumption
> that the downcasing conversion doesn't change the string's physical
> length.  There are a lot of counterexamples to that :-(.  I'm not sure

Mmm. I didn't know of that.

> that we actually implement such cases correctly today, but let's not
> build APIs that prevent it from being fixed.

Agreed. Thanks for the knowledge.

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
Kyotaro Horiguchi <horikyota.ntt@gmail.com> writes:
> At Thu, 22 Apr 2021 23:17:19 -0400, Tom Lane <tgl@sss.pgh.pa.us> wrote in 
>> Doesn't seem like a good idea, because that locks us into an assumption
>> that the downcasing conversion doesn't change the string's physical
>> length.  There are a lot of counterexamples to that :-(.  I'm not sure

> Mmm. I didn't know of that.

The two examples I know of offhand are in German (eszett "ß" downcases to
"ss") and Turkish (dotted "Í" downcases to "i", likewise dotless "I"
downcases to "ı"; one of each of those pairs is an ASCII letter, the
other is not).  Depending on which encoding is in use, these
transformations *could* be the same number of bytes, but they could
equally well not be.  There are probably other examples.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Kyotaro Horiguchi
Дата:
FWIW...

At Fri, 23 Apr 2021 00:17:35 -0400, Tom Lane <tgl@sss.pgh.pa.us> wrote in 
> Kyotaro Horiguchi <horikyota.ntt@gmail.com> writes:
> > At Thu, 22 Apr 2021 23:17:19 -0400, Tom Lane <tgl@sss.pgh.pa.us> wrote in 
> >> Doesn't seem like a good idea, because that locks us into an assumption
> >> that the downcasing conversion doesn't change the string's physical
> >> length.  There are a lot of counterexamples to that :-(.  I'm not sure
> 
> > Mmm. I didn't know of that.
> 
> The two examples I know of offhand are in German (eszett "ß" downcases to
> "ss") and Turkish (dotted "Í" downcases to "i", likewise dotless "I"

According to Wikipedia, "ss" is equivalent to "ß" and their upper case
letters are "SS" and "ẞ" respectively. (I didn't even know of the
existence of "ẞ". AFAIK there's no word begins with eszett, but it
seems that there's a case where "ẞ" appears in a word is spelled only
with capital letters.

> downcases to "ı"; one of each of those pairs is an ASCII letter, the
> other is not).  Depending on which encoding is in use, these

Upper dotless "I" and lower dotted "i" are in ASCII (or English
alphabet?).  That's interesting.

> transformations *could* be the same number of bytes, but they could
> equally well not be.  There are probably other examples.

Yeah. Agreed.

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center

RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
Hi

I've updated the patch to V7 based on the following comments.

On Friday, April 23, 2021 11:58 AM, Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote
>All usages of pg_string_tolower don't need a copy.
>So don't we change the function to in-place converter?

Refer to your later discussion with Tom. Keep the code as it is.

>        if (completion_info_charp)
>+        {
>             e_info_charp = escape_string(completion_info_charp);
>+            if(e_info_charp[0] == '"')
>+                completion_case_sensitive = true;
>+            else
>+            {
>+                le_str = pg_string_tolower(e_info_charp);
>
>It seems right to lower completion_info_charp and ..2 but it is not
>right that change completion_case_sensitive here, which only affects
>the returned candidates.

Agreed, code " completion_case_sensitive = true;" removed.

>By the way COMP_KEYWORD_CASE suggests that *keywords* are completed
>following the setting. However, they are not keywords, but
>identifiers. And some people (including me) might dislike that
>keywords and identifiers follow the same setting.  Specifically I
>sometimes want keywords to be upper-cased but identifiers (always) be
>lower-cased.

Changed my design based on your suggestion. Now the upper character inputs for identifiers will always turn to lower
case(regardlessCOMP_KEYWORD_CASE) which I think can be accepted by most of PG users.  
  Eg: SET BYT<tab> / SET Byt<tab>
  output when apply V6 patch: SET BYTEA_OUTPUT
  output when apply V7 patch: SET bytea_output

On Friday, April 23, 2021 12:26 PM, Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote
>Oh, I accidentally found a doubious behsbior.
>
>=# alter table public.<tab>
>public.c1    public.d1    public."t"   public.t     public."tt"
>
>The "t" and "tt" are needlessly lower-cased.

Good catch. I didn’t think of schema stuff before.
Bug fixed. Add tap tests for this scenario.

Please let me know if you find more insufficient issue in the patch. Any further suggestion is very welcome.

Regards,
Tang

Вложения

RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
Hi

I've updated the patch to V8 since Tom, Kyotaro and Laurenz discussed the lower case issue of German/Turkish language
at[1]. 

Differences from V7 are:
* Add a function valid_input_text which checks the input text to see if it only contains alphabet letters, numbers etc.
* Delete the flag setting of "completion_case_sensitive=false" which introduced in V1 patch and no use now.

As you can see, now the patch limited the lower case transform of the input to alphabet letters.
By doing that, language like German/Turkish will not affected by this patch.

Any comment or suggestion on this patch is very welcome.

[1]
https://www.postgresql.org/message-id/1282887.1619151455%40sss.pgh.pa.us
https://www.postgresql.org/message-id/20210423.144443.2058612313278551429.horikyota.ntt%40gmail.com
https://www.postgresql.org/message-id/a75a6574c0e3d4773ba20a73d493c2c9983c0657.camel%40cybertec.at

Regards,
Tang


Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 23.06.21 14:43, tanghy.fnst@fujitsu.com wrote:
> I've updated the patch to V8 since Tom, Kyotaro and Laurenz discussed the lower case issue of German/Turkish language
at[1].
 
> 
> Differences from V7 are:
> * Add a function valid_input_text which checks the input text to see if it only contains alphabet letters, numbers
etc.
> * Delete the flag setting of "completion_case_sensitive=false" which introduced in V1 patch and no use now.
> 
> As you can see, now the patch limited the lower case transform of the input to alphabet letters.
> By doing that, language like German/Turkish will not affected by this patch.
> 
> Any comment or suggestion on this patch is very welcome.

The coding of valid_input_text() seems a bit bulky.  I think you can do  
the same thing using strspn() without a loop.

The name is also not great.  It's not like other strings are not "valid".

There is also no explanation why that specific set of characters is  
allowed and not others.  Does it have something to do with identifier  
syntax?  This needs to be explained.

Seeing that valid_input_text() is always called together with  
pg_string_tolower(), I think those could be combined into one function,  
like pg_string_tolower_if_ascii() is whatever.  That would save a lot of  
repetition.

There are a couple of queries where the result is *not*  
case-insensitive, namely

Query_for_list_of_enum_values
Query_for_list_of_available_extension_versions

(and their variants).  These are cases where the query result is not  
used as an identifier but as a (single-quoted) string.  So that needs to  
be handled somehow, perhaps by adding a COMPLETE_WITH_QUERY_CS() similar  
to COMPLETE_WITH_CS().

(A test case for the enum case should be doable easily.)



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Tuesday, September 7, 2021 5:25 PM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:
>The coding of valid_input_text() seems a bit bulky.  I think you can do
>the same thing using strspn() without a loop.

Thanks, modified in V9 patch.

>The name is also not great.  It's not like other strings are not "valid".

Modified.
valid_input_text() renamed to check_input_text()

>There is also no explanation why that specific set of characters is
>allowed and not others.  Does it have something to do with identifier
>syntax?  This needs to be explained.

Added some comments for pg_string_tolower_if_ascii().
For language like German/Turkish, it's not a good idea to lower the input text
because the upper case words may not retain the same meaning.(Pointed at [1~3])

>Seeing that valid_input_text() is always called together with
>pg_string_tolower(), I think those could be combined into one function,
>like pg_string_tolower_if_ascii() is whatever.  That would save a lot of
>repetition.

Modified.

>There are a couple of queries where the result is *not*
>case-insensitive, namely
>
>Query_for_list_of_enum_values
>Query_for_list_of_available_extension_versions
>
>(and their variants).  These are cases where the query result is not
>used as an identifier but as a (single-quoted) string.  So that needs to
>be handled somehow, perhaps by adding a COMPLETE_WITH_QUERY_CS() similar
>to COMPLETE_WITH_CS().

Hmm, I think 'a (single-quoted) string' identifier behaves the same way with or without my patch.
Could your please give me an example on that?(to help me figure out why we need something like
COMPLETE_WITH_QUERY_CS())

>(A test case for the enum case should be doable easily.)

Test added.

BTW, I found tap completion for enum value is not perfect on HEAD.
Maybe I will fix this problem in another thread.

example:
=# create type pp_colors as enum ('green', 'blue', 'black');
=# ALTER TYPE pp_colors RENAME VALUE 'b[tab]
=# alter type pp_colors rename value 'b'   <- blue is not auto completed as expected

[1] https://www.postgresql.org/message-id/1282887.1619151455%40sss.pgh.pa.us
[2] https://www.postgresql.org/message-id/20210423.144443.2058612313278551429.horikyota.ntt%40gmail.com
[3] https://www.postgresql.org/message-id/a75a6574c0e3d4773ba20a73d493c2c9983c0657.camel%40cybertec.at

Regards,
Tang


Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 10.09.21 15:50, tanghy.fnst@fujitsu.com wrote:
>> (A test case for the enum case should be doable easily.)
> Test added.

The enum test is failing on *some* platforms:

t/010_tab_completion.pl .. 26/?
#   Failed test 'complete enum values'
#   at t/010_tab_completion.pl line 211.
# Actual output was "ALTER TYPE mytype1 RENAME VALUE '\a\r\n'BLUE' 
'bLACK'  'green'  \r\npostgres=# ALTER TYPE mytype1 RENAME VALUE '"
# Did not match "(?^:'bLACK' + 'BLUE' + 'green')"

So the ordering of the suggested completions is different.  I don't know 
offhand how that ordering is determined.  Perhaps it's dependent on 
locale, readline version, or operating system.  In any case, we need to 
figure this out to make this test stable.



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:
> So the ordering of the suggested completions is different.  I don't know 
> offhand how that ordering is determined.  Perhaps it's dependent on 
> locale, readline version, or operating system.  In any case, we need to 
> figure this out to make this test stable.

I don't think we want to get into the business of trying to make that
consistent across different readline/libedit versions.  How about
adjusting the test case so that only one enum value is to be printed?

            regards, tom lane



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Thursday, January 6, 2022 11:57 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
>
> Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:
> > So the ordering of the suggested completions is different.  I don't know
> > offhand how that ordering is determined.  Perhaps it's dependent on
> > locale, readline version, or operating system.  In any case, we need to
> > figure this out to make this test stable.
>
> I don't think we want to get into the business of trying to make that
> consistent across different readline/libedit versions.  How about
> adjusting the test case so that only one enum value is to be printed?
>

Thanks for your suggestion. Agreed.
Fixed the test case to show only one enum value.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Japin Li
Дата:
On Fri, 07 Jan 2022 at 10:12, tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com> wrote:
> On Thursday, January 6, 2022 11:57 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
>>
>> Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:
>> > So the ordering of the suggested completions is different.  I don't know
>> > offhand how that ordering is determined.  Perhaps it's dependent on
>> > locale, readline version, or operating system.  In any case, we need to
>> > figure this out to make this test stable.
>>
>> I don't think we want to get into the business of trying to make that
>> consistent across different readline/libedit versions.  How about
>> adjusting the test case so that only one enum value is to be printed?
>>
>
> Thanks for your suggestion. Agreed.
> Fixed the test case to show only one enum value.
>

+/*
+ * pg_string_tolower - Fold a string to lower case if the string is not quoted
+ * and only contains ASCII characters.
+ * For German/Turkish etc text, no change will be made.
+ *
+ * The returned value has to be freed.
+ */
+static char *
+pg_string_tolower_if_ascii(const char *text)
+{

s/pg_string_tolower/pg_string_tolower_if_ascii/ for comments.

--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Friday, January 7, 2022 1:08 PM, Japin Li <japinli@hotmail.com> wrote:
> +/*
> + * pg_string_tolower - Fold a string to lower case if the string is not quoted
> + * and only contains ASCII characters.
> + * For German/Turkish etc text, no change will be made.
> + *
> + * The returned value has to be freed.
> + */
> +static char *
> +pg_string_tolower_if_ascii(const char *text)
> +{
>
> s/pg_string_tolower/pg_string_tolower_if_ascii/ for comments.
>

Thanks for your review.
Comment fixed in the attached V11 patch.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 07.01.22 06:17, tanghy.fnst@fujitsu.com wrote:
> On Friday, January 7, 2022 1:08 PM, Japin Li <japinli@hotmail.com> wrote:
>> +/*
>> + * pg_string_tolower - Fold a string to lower case if the string is not quoted
>> + * and only contains ASCII characters.
>> + * For German/Turkish etc text, no change will be made.
>> + *
>> + * The returned value has to be freed.
>> + */
>> +static char *
>> +pg_string_tolower_if_ascii(const char *text)
>> +{
>>
>> s/pg_string_tolower/pg_string_tolower_if_ascii/ for comments.
>>
> 
> Thanks for your review.
> Comment fixed in the attached V11 patch.

As I just posted over at [0], the tab completion of enum values appears 
to be broken at the moment, so I can't really analyze what impact your 
patch would have on it.  (But it makes me suspicious about the test case 
in your patch.)  I suspect it would treat enum labels as 
case-insensitive, which would be wrong.  But we need to fix that issue 
first before we can proceed here.

The rest of the patch seems ok in principle, since AFAICT enums are the 
only query result in tab-complete.c that are not identifiers and thus 
subject to case issues.

I would perhaps move the pg_string_tolower_if_ascii() calls to before 
escape_string() in each case.  It won't make a difference to the result, 
but it seems conceptually better.


[0]: 
https://www.postgresql.org/message-id/8ca82d89-ec3d-8b28-8291-500efaf23b25@enterprisedb.com



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:
> The rest of the patch seems ok in principle, since AFAICT enums are the 
> only query result in tab-complete.c that are not identifiers and thus 
> subject to case issues.

I spent some time looking at this patch.  I'm not very happy with it,
for two reasons:

1. The downcasing logic in the patch bears very little resemblance
to the backend's actual downcasing logic, which can be found in
src/backend/parser/scansup.c's downcase_identifier().  Notably,
the patch's restriction to only convert all-ASCII strings seems
indefensible, because that's not how things really work.  I fear
we can't always exactly duplicate the backend's behavior, because
it's dependent on the server's locale and encoding; but I think
we should at least get it right in the common case where psql is
using the same locale and encoding as the server.

2. I don't think there's been much thought about the larger picture
of what is to be accomplished.  Right now, we successfully
tab-complete inputs that are prefixes of the canonical spelling (per
quote_identifier) of the object's name, and don't try at all for
non-canonical spellings.  I'm on board with trying to allow some of
the latter but I'm not sure that this patch represents much forward
progress.  To be definite about it, suppose we have a DB containing
just two tables whose names start with "m", say mytab and mixedTab.
Then:

(a) m<TAB> immediately completes mytab, ignoring mixedTab

(b) "m<TAB> immediately completes "mixedTab", ignoring mytab

(c) "my<TAB> fails to find anything

(d) mi<TAB> fails to find anything

(e) M<TAB> fails to find anything

This patch proposes to improve case (e), but to my taste cases (a)
through (c) are much bigger problems.  It'd be nice if (d) worked too
--- that'd require injecting a double-quote where the user had not
typed one, but we already do the equivalent thing with single-quotes
for file names, so why not?  (Although after fighting with readline
yesterday to try to get it to handle single-quoted enum labels sanely,
I'm not 100% sure if (d) is possible.)

Also, even for case (e), what we have with this patch is that it
immediately completes mytab, ignoring mixedTab.  Is that what we want?
Another example is that miX<TAB> fails to find anything, which seems
like a POLA violation given that mY<TAB> completes to mytab.

I'm not certain how many of these alternatives can be supported
without introducing ambiguity that wasn't there before (which'd
manifest as failing to complete in cases where the existing code
chooses an alternative just fine).  But I really don't like the
existing behavior for (b) and (c) --- I should be able to spell
a name with double quotes if I want, without losing completion
support.

BTW, another thing that maybe we should think about is how this
interacts with the pattern matching capability in \d and friends.
If people can tab-complete non-canonical spellings, they might
expect the same spellings to work in \d.  I don't say that this
patch has to fix that, but we might want to look and be sure we're
not painting ourselves into a corner (especially since I see
that we already perform tab-completion in that context).

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Julien Rouhaud
Дата:
Hi,

On Sat, Jan 15, 2022 at 01:51:26PM -0500, Tom Lane wrote:
> 
> I spent some time looking at this patch.  I'm not very happy with it,
> for two reasons:
> [...]

On top of that the patch doesn't apply anymore:

http://cfbot.cputube.org/patch_36_2979.log
=== Applying patches on top of PostgreSQL commit ID 5987feb70b5bbb1fc4e64d433f490df08d91dd45 ===
=== applying patch ./v11-0001-Support-tab-completion-with-a-query-result-for-u.patch
patching file src/bin/psql/t/010_tab_completion.pl
Hunk #1 FAILED at 41.
Hunk #2 succeeded at 150 (offset 1 line).
1 out of 2 hunks FAILED -- saving rejects to file src/bin/psql/t/010_tab_completion.pl.rej

I'm switching the CF entry to Waiting on Author.



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Sunday, January 16, 2022 3:51 AM, Tom Lane <tgl@sss.pgh.pa.us> said:
> Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:
> > The rest of the patch seems ok in principle, since AFAICT enums are the
> > only query result in tab-complete.c that are not identifiers and thus
> > subject to case issues.
>
> I spent some time looking at this patch.  I'm not very happy with it,
> for two reasons:
>
> 1. The downcasing logic in the patch bears very little resemblance
> to the backend's actual downcasing logic, which can be found in
> src/backend/parser/scansup.c's downcase_identifier().  Notably,
> the patch's restriction to only convert all-ASCII strings seems
> indefensible, because that's not how things really work.  I fear
> we can't always exactly duplicate the backend's behavior, because
> it's dependent on the server's locale and encoding; but I think
> we should at least get it right in the common case where psql is
> using the same locale and encoding as the server.

Thanks for your suggestion, I removed ASCII strings check function
and added single byte encoding check just like downcase_identifier.
Also added PGCLIENTENCODING setting in the test script to make
test cases pass.
Now the patch supports tab-completion with none-quoted upper characters
available when client encoding is in single byte.

> 2. I don't think there's been much thought about the larger picture
> of what is to be accomplished.  Right now, we successfully
> tab-complete inputs that are prefixes of the canonical spelling (per
> quote_identifier) of the object's name, and don't try at all for
> non-canonical spellings.  I'm on board with trying to allow some of
> the latter but I'm not sure that this patch represents much forward
> progress.  To be definite about it, suppose we have a DB containing
> just two tables whose names start with "m", say mytab and mixedTab.
> Then:
>
> (a) m<TAB> immediately completes mytab, ignoring mixedTab
>
> (b) "m<TAB> immediately completes "mixedTab", ignoring mytab
>
> (c) "my<TAB> fails to find anything
>
> (d) mi<TAB> fails to find anything
>
> (e) M<TAB> fails to find anything
>
> This patch proposes to improve case (e), but to my taste cases (a)
> through (c) are much bigger problems.  It'd be nice if (d) worked too
> --- that'd require injecting a double-quote where the user had not
> typed one, but we already do the equivalent thing with single-quotes
> for file names, so why not?  (Although after fighting with readline
> yesterday to try to get it to handle single-quoted enum labels sanely,
> I'm not 100% sure if (d) is possible.)
>
> Also, even for case (e), what we have with this patch is that it
> immediately completes mytab, ignoring mixedTab.  Is that what we want?
> Another example is that miX<TAB> fails to find anything, which seems
> like a POLA violation given that mY<TAB> completes to mytab.
>
> I'm not certain how many of these alternatives can be supported
> without introducing ambiguity that wasn't there before (which'd
> manifest as failing to complete in cases where the existing code
> chooses an alternative just fine).  But I really don't like the
> existing behavior for (b) and (c) --- I should be able to spell
> a name with double quotes if I want, without losing completion
> support.

You are right, it's more convenient in that way.
I haven't thought about it before. By now, the patch suppose:
If user needs to type a table with name in upper character,
they should input the double quotes by themselves. If the double
quote is input by a user, only table name with upper character could be searched.

I may try to implement as you expected but it seems not so easy.
(as you said, without introducing ambiguity that wasn't there before)
I'd appreciate if someone could give me a hint/hand on this.

> BTW, another thing that maybe we should think about is how this
> interacts with the pattern matching capability in \d and friends.
> If people can tab-complete non-canonical spellings, they might
> expect the same spellings to work in \d.  I don't say that this
> patch has to fix that, but we might want to look and be sure we're
> not painting ourselves into a corner (especially since I see
> that we already perform tab-completion in that context).

Yes. Agreed, if we solve the previous problem,
meta-command tab completion should also be considered.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Peter Eisentraut
Дата:
On 20.01.22 08:37, tanghy.fnst@fujitsu.com wrote:
>> 1. The downcasing logic in the patch bears very little resemblance
>> to the backend's actual downcasing logic, which can be found in
>> src/backend/parser/scansup.c's downcase_identifier().  Notably,
>> the patch's restriction to only convert all-ASCII strings seems
>> indefensible, because that's not how things really work.  I fear
>> we can't always exactly duplicate the backend's behavior, because
>> it's dependent on the server's locale and encoding; but I think
>> we should at least get it right in the common case where psql is
>> using the same locale and encoding as the server.
> Thanks for your suggestion, I removed ASCII strings check function
> and added single byte encoding check just like downcase_identifier.
> Also added PGCLIENTENCODING setting in the test script to make
> test cases pass.
> Now the patch supports tab-completion with none-quoted upper characters
> available when client encoding is in single byte.

The way your patch works now is that the case-insensitive behavior you 
are implementing only works if the client encoding is a single-byte 
encoding.  This isn't what downcase_identifier() does; 
downcase_identifier() always works for ASCII characters.  As it is, this 
patch is nearly useless, since very few people use single-byte client 
encodings anymore.  Also, I think it would be highly confusing if the 
tab completion behavior depended on the client encoding in a significant 
way.

Also, as I had previously suspected, your patch treats the completion of 
enum labels in a case-insensitive way (since it all goes through 
_complete_from_query()), but enum labels are not case insensitive.  You 
can observe this behavior using this test case:

+check_completion("ALTER TYPE enum1 RENAME VALUE 'F\t\t", qr|foo|, "FIXME");
+
+clear_line();

You should devise a principled way to communicate to 
_complete_from_query() whether it should do case-sensitive or 
-insensitive completion.  We already have COMPLETE_WITH() and 
COMPLETE_WITH_CS() etc. to do this in other cases, so it should be 
straightforward to adapt a similar system.



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Monday, January 24, 2022 6:36 PM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:
> The way your patch works now is that the case-insensitive behavior you
> are implementing only works if the client encoding is a single-byte
> encoding.  This isn't what downcase_identifier() does;
> downcase_identifier() always works for ASCII characters.  As it is, this
> patch is nearly useless, since very few people use single-byte client
> encodings anymore.  Also, I think it would be highly confusing if the
> tab completion behavior depended on the client encoding in a significant
> way.

Thanks for your review. I misunderstood the logic of downcase_identifier().
Modified the code to support ASCII characters input. 

> Also, as I had previously suspected, your patch treats the completion of
> enum labels in a case-insensitive way (since it all goes through
> _complete_from_query()), but enum labels are not case insensitive.  You
> can observe this behavior using this test case:
> 
> +check_completion("ALTER TYPE enum1 RENAME VALUE 'F\t\t", qr|foo|, "FIXME");
> +
> +clear_line();

Your suspect is correct. I didn't aware enum labels are case sensitive.
I've added this test to the tap tests. 

> You should devise a principled way to communicate to
> _complete_from_query() whether it should do case-sensitive or
> -insensitive completion.  We already have COMPLETE_WITH() and
> COMPLETE_WITH_CS() etc. to do this in other cases, so it should be
> straightforward to adapt a similar system.

I tried to add a flag(casesensitive) in the _complete_from_query().
Now the attached patch passed all the added tap tests.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Julien Rouhaud
Дата:
Hi,

On Tue, Jan 25, 2022 at 05:22:32AM +0000, tanghy.fnst@fujitsu.com wrote:
> 
> I tried to add a flag(casesensitive) in the _complete_from_query().
> Now the attached patch passed all the added tap tests.

Thanks for updating the patch.  When you do so, please check and update the
commitfest entry accordingly to make sure that people knows it's ready for
review.  I'm switching the entry to Needs Review.



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
I spent some time contemplating my navel about the concerns I raised
upthread about double-quoted identifiers.  I concluded that the reason
things don't work well in that area is that we're trying to get all the
work done by applying quote_ident() on the backend side and then
ignoring quoting considerations in tab-complete itself.  That sort of
works, but not terribly well.  The currently proposed patch is sticking
a toe into the water of dealing with quoting/downcasing in tab-complete,
but we need to go a lot further.  I propose that we ought to drop the
use of quote_ident() in the tab completion queries altogether, instead
having the backend return names as-is, and doing all the dequoting and
requoting work in tab-complete.

Attached is a very-much-WIP patch along these lines.  I make no
pretense that it's complete; no doubt some of the individual
queries are broken or don't return quite the results we want.
But it seems to act the way I think it should for relation names.

One thing I'm particularly unsure what to do with is the queries
for type names, which want to match against the output of
format_type, which'll already have applied quote_ident.  We can
probably hack something up there, but I ran out of time to mess
with that for today.

Anyway, I wanted to post this just to see what people think of
going in this direction.

            regards, tom lane

PS: I omitted the proposed regression test changes here.
Many of them are not at all portable --- different versions
of readline/libedit will produce different control character
sequences for backspacing, for example.  I got a lot of
failures when I tried to use those tests with this patch;
I've not run down which ones are test portability problems,
which are due to intentional behavior changes in this patch,
and which are due to breakage I've not fixed yet.

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 502b5c5751..2dadf7d945 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_class_d.h"
 #include "common.h"
 #include "libpq-fe.h"
+#include "mb/pg_wchar.h"
 #include "pqexpbuffer.h"
 #include "settings.h"
 #include "stringutils.h"
@@ -148,8 +149,8 @@ typedef struct SchemaQuery
     const char *namespace;

     /*
-     * Result --- the appropriately-quoted name to return, in the case of an
-     * unqualified name.  For example, "pg_catalog.quote_ident(c.relname)".
+     * Result --- the (unquoted) name to return, in the case of an unqualified
+     * name.  For example, "c.relname".
      */
     const char *result;

@@ -315,7 +316,7 @@ do { \
         completion_info_charp = _completion_type; \
         completion_info_charp2 = _completion_schema; \
     } \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_query_verbatim); \
 } while (0)

 #define COMPLETE_WITH_FUNCTION_ARG(function) \
@@ -357,14 +358,14 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
         .selcondition = "p.prokind = 'a'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .selcondition = "p.proisagg",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

@@ -378,7 +379,7 @@ static const SchemaQuery Query_for_list_of_datatypes = {
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
     .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .qualresult = "t.typname",
 };

 static const SchemaQuery Query_for_list_of_composite_datatypes = {
@@ -390,7 +391,7 @@ static const SchemaQuery Query_for_list_of_composite_datatypes = {
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
     .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .qualresult = "t.typname",
 };

 static const SchemaQuery Query_for_list_of_domains = {
@@ -398,7 +399,7 @@ static const SchemaQuery Query_for_list_of_domains = {
     .selcondition = "t.typtype = 'd'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 /* Note: this intentionally accepts aggregates as well as plain functions */
@@ -409,13 +410,13 @@ static const SchemaQuery Query_for_list_of_functions[] = {
         .selcondition = "p.prokind != 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

@@ -426,7 +427,7 @@ static const SchemaQuery Query_for_list_of_procedures[] = {
         .selcondition = "p.prokind = 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         /* not supported in older versions */
@@ -438,7 +439,7 @@ static const SchemaQuery Query_for_list_of_routines = {
     .catname = "pg_catalog.pg_proc p",
     .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
     .namespace = "p.pronamespace",
-    .result = "pg_catalog.quote_ident(p.proname)",
+    .result = "p.proname",
 };

 static const SchemaQuery Query_for_list_of_sequences = {
@@ -446,7 +447,7 @@ static const SchemaQuery Query_for_list_of_sequences = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_SEQUENCE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_foreign_tables = {
@@ -454,7 +455,7 @@ static const SchemaQuery Query_for_list_of_foreign_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_tables = {
@@ -464,7 +465,7 @@ static const SchemaQuery Query_for_list_of_tables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_tables = {
@@ -472,7 +473,7 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_views = {
@@ -480,7 +481,7 @@ static const SchemaQuery Query_for_list_of_views = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_matviews = {
@@ -488,7 +489,7 @@ static const SchemaQuery Query_for_list_of_matviews = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_indexes = {
@@ -498,7 +499,7 @@ static const SchemaQuery Query_for_list_of_indexes = {
     CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_indexes = {
@@ -506,7 +507,7 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
     .selcondition = "c.relkind = " CppAsString2(RELKIND_PARTITIONED_INDEX),
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };


@@ -515,7 +516,7 @@ static const SchemaQuery Query_for_list_of_relations = {
     .catname = "pg_catalog.pg_class c",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* partitioned relations */
@@ -525,14 +526,14 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = {
     ", " CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_operator_families = {
     .catname = "pg_catalog.pg_opfamily c",
     .viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)",
     .namespace = "c.opfnamespace",
-    .result = "pg_catalog.quote_ident(c.opfname)",
+    .result = "c.opfname",
 };

 /* Relations supporting INSERT, UPDATE or DELETE */
@@ -545,7 +546,7 @@ static const SchemaQuery Query_for_list_of_updatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting SELECT */
@@ -560,7 +561,7 @@ static const SchemaQuery Query_for_list_of_selectables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting TRUNCATE */
@@ -572,7 +573,7 @@ static const SchemaQuery Query_for_list_of_truncatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting GRANT are currently same as those supporting SELECT */
@@ -588,7 +589,7 @@ static const SchemaQuery Query_for_list_of_analyzables = {
     CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting index creation */
@@ -600,7 +601,7 @@ static const SchemaQuery Query_for_list_of_indexables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /*
@@ -617,7 +618,7 @@ static const SchemaQuery Query_for_list_of_clusterables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
@@ -625,14 +626,14 @@ static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .selcondition = "c.conrelid <> 0",
     .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
-    .result = "pg_catalog.quote_ident(c.conname)",
+    .result = "c.conname",
 };

 static const SchemaQuery Query_for_list_of_statistics = {
     .catname = "pg_catalog.pg_statistic_ext s",
     .viscondition = "pg_catalog.pg_statistics_obj_is_visible(s.oid)",
     .namespace = "s.stxnamespace",
-    .result = "pg_catalog.quote_ident(s.stxname)",
+    .result = "s.stxname",
 };

 static const SchemaQuery Query_for_list_of_collations = {
@@ -640,7 +641,7 @@ static const SchemaQuery Query_for_list_of_collations = {
     .selcondition = "c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))",
     .viscondition = "pg_catalog.pg_collation_is_visible(c.oid)",
     .namespace = "c.collnamespace",
-    .result = "pg_catalog.quote_ident(c.collname)",
+    .result = "c.collname",
 };


@@ -659,13 +660,13 @@ static const SchemaQuery Query_for_list_of_collations = {
  */

 #define Query_for_list_of_attributes \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
 " WHERE c.oid = a.attrelid "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

@@ -676,21 +677,21 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
 "   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

 #define Query_for_list_of_attributes_with_schema \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
 " WHERE c.oid = a.attrelid "\
 "   AND n.oid = c.relnamespace "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_quoted \
@@ -698,7 +699,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
 " WHERE t.oid = e.enumtypid "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -707,7 +708,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  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' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -717,9 +718,9 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_with_schema_unquoted \
@@ -728,24 +729,24 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(enumlabel,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_template_databases \
-"SELECT pg_catalog.quote_ident(d.datname) "\
+"SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(pg_catalog.quote_ident(d.datname),1,%d)='%s' "\
+" WHERE substring(d.datname,1,%d)='%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
-"SELECT pg_catalog.quote_ident(datname) FROM pg_catalog.pg_database "\
-" WHERE substring(pg_catalog.quote_ident(datname),1,%d)='%s'"
+"SELECT datname FROM pg_catalog.pg_database "\
+" WHERE substring(datname,1,%d)='%s'"

 #define Query_for_list_of_tablespaces \
-"SELECT pg_catalog.quote_ident(spcname) FROM pg_catalog.pg_tablespace "\
-" WHERE substring(pg_catalog.quote_ident(spcname),1,%d)='%s'"
+"SELECT spcname FROM pg_catalog.pg_tablespace "\
+" WHERE substring(spcname,1,%d)='%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
@@ -753,14 +754,14 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"

 #define Query_for_list_of_languages \
-"SELECT pg_catalog.quote_ident(lanname) "\
+"SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(pg_catalog.quote_ident(lanname),1,%d)='%s'"
+"   AND substring(lanname,1,%d)='%s'"

 #define Query_for_list_of_schemas \
-"SELECT pg_catalog.quote_ident(nspname) FROM pg_catalog.pg_namespace "\
-" WHERE substring(pg_catalog.quote_ident(nspname),1,%d)='%s'"
+"SELECT nspname FROM pg_catalog.pg_namespace "\
+" WHERE substring(nspname,1,%d)='%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
@@ -789,14 +790,14 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
+" SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"
+"  WHERE substring(rolname,1,%d)='%s'"

 #define Query_for_list_of_grant_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
+" SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"\
+"  WHERE substring(rolname,1,%d)='%s'"\
 " UNION ALL SELECT 'PUBLIC'"\
 " UNION ALL SELECT 'CURRENT_ROLE'"\
 " UNION ALL SELECT 'CURRENT_USER'"\
@@ -804,11 +805,11 @@ static const SchemaQuery Query_for_list_of_collations = {

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_index_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
 " WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
 "       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"

 #define Query_for_unique_index_of_table \
@@ -817,124 +818,124 @@ Query_for_index_of_table \

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
 " WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table_not_validated \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
 " WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)" \
 "       and not con.convalidated"

 #define Query_for_all_table_constraints \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_type \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
 " WHERE t.oid=contypid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(t.typname)='%s'"\
+"       and t.typname='%s'"\
 "       and pg_catalog.pg_type_is_visible(t.oid)"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_constraint \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
 " WHERE (%d = pg_catalog.length('%s'))"\
 "   AND oid IN "\
 "       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE pg_catalog.quote_ident(conname)='%s')"
+"         WHERE conname='%s')"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_rule_of_table \
-"SELECT pg_catalog.quote_ident(rulename) "\
+"SELECT rulename "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
 " WHERE c1.oid=ev_class and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_rule \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
 " WHERE (%d = pg_catalog.length('%s'))"\
 "   AND oid IN "\
 "       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE pg_catalog.quote_ident(rulename)='%s')"
+"         WHERE rulename='%s')"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_trigger_of_table \
-"SELECT pg_catalog.quote_ident(tgname) "\
+"SELECT tgname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
 " WHERE c1.oid=tgrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"\
 "       and not tgisinternal"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_trigger \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
 " WHERE (%d = pg_catalog.length('%s'))"\
 "   AND oid IN "\
 "       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE pg_catalog.quote_ident(tgname)='%s')"
+"         WHERE tgname='%s')"

 #define Query_for_list_of_ts_configurations \
-"SELECT pg_catalog.quote_ident(cfgname) FROM pg_catalog.pg_ts_config "\
-" WHERE substring(pg_catalog.quote_ident(cfgname),1,%d)='%s'"
+"SELECT cfgname FROM pg_catalog.pg_ts_config "\
+" WHERE substring(cfgname,1,%d)='%s'"

 #define Query_for_list_of_ts_dictionaries \
-"SELECT pg_catalog.quote_ident(dictname) FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(pg_catalog.quote_ident(dictname),1,%d)='%s'"
+"SELECT dictname FROM pg_catalog.pg_ts_dict "\
+" WHERE substring(dictname,1,%d)='%s'"

 #define Query_for_list_of_ts_parsers \
-"SELECT pg_catalog.quote_ident(prsname) FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(pg_catalog.quote_ident(prsname),1,%d)='%s'"
+"SELECT prsname FROM pg_catalog.pg_ts_parser "\
+" WHERE substring(prsname,1,%d)='%s'"

 #define Query_for_list_of_ts_templates \
-"SELECT pg_catalog.quote_ident(tmplname) FROM pg_catalog.pg_ts_template "\
-" WHERE substring(pg_catalog.quote_ident(tmplname),1,%d)='%s'"
+"SELECT tmplname FROM pg_catalog.pg_ts_template "\
+" WHERE substring(tmplname,1,%d)='%s'"

 #define Query_for_list_of_fdws \
-" SELECT pg_catalog.quote_ident(fdwname) "\
+" SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(pg_catalog.quote_ident(fdwname),1,%d)='%s'"
+"  WHERE substring(fdwname,1,%d)='%s'"

 #define Query_for_list_of_servers \
-" SELECT pg_catalog.quote_ident(srvname) "\
+" SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(pg_catalog.quote_ident(srvname),1,%d)='%s'"
+"  WHERE substring(srvname,1,%d)='%s'"

 #define Query_for_list_of_user_mappings \
-" SELECT pg_catalog.quote_ident(usename) "\
+" SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(pg_catalog.quote_ident(usename),1,%d)='%s'"
+"  WHERE substring(usename,1,%d)='%s'"

 #define Query_for_list_of_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s'"
+"  WHERE substring(amname,1,%d)='%s'"

 #define Query_for_list_of_index_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

 /* the silly-looking length condition is just to eat up the current word */
@@ -942,7 +943,7 @@ Query_for_index_of_table \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc "\
 " WHERE (%d = pg_catalog.length('%s'))"\
-"   AND (pg_catalog.quote_ident(proname)='%s'"\
+"   AND (proname='%s'"\
 "        OR '\"' || proname || '\"'='%s') "\
 "   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"

@@ -952,68 +953,68 @@ Query_for_index_of_table \
 "  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
 " WHERE (%d = pg_catalog.length('%s'))"\
 "   AND n.oid = p.pronamespace "\
-"   AND (pg_catalog.quote_ident(proname)='%s' "\
+"   AND (proname='%s' "\
 "        OR '\"' || proname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_extensions \
-" SELECT pg_catalog.quote_ident(extname) "\
+" SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(pg_catalog.quote_ident(extname),1,%d)='%s'"
+"  WHERE substring(extname,1,%d)='%s'"

 #define Query_for_list_of_available_extensions \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s' AND installed_version IS NULL"
+"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_available_extension_versions \
-" SELECT pg_catalog.quote_ident(version) "\
+" SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
 "  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"    AND name='%s'"

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_available_extension_versions_with_TO \
-" SELECT 'TO ' || pg_catalog.quote_ident(version) "\
+" SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
 "  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_event_triggers \
-" SELECT pg_catalog.quote_ident(evtname) "\
+" SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(pg_catalog.quote_ident(evtname),1,%d)='%s'"
+"  WHERE substring(evtname,1,%d)='%s'"

 #define Query_for_list_of_tablesample_methods \
-" SELECT pg_catalog.quote_ident(proname) "\
+" SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(pg_catalog.quote_ident(proname),1,%d)='%s'"
+"        substring(proname,1,%d)='%s'"

 #define Query_for_list_of_policies \
-" SELECT pg_catalog.quote_ident(polname) "\
+" SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(pg_catalog.quote_ident(polname),1,%d)='%s'"
+"  WHERE substring(polname,1,%d)='%s'"

 #define Query_for_list_of_tables_for_policy \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
 " WHERE (%d = pg_catalog.length('%s'))"\
 "   AND oid IN "\
 "       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE pg_catalog.quote_ident(polname)='%s')"
+"         WHERE polname='%s')"

 #define Query_for_enum \
 " SELECT name FROM ( "\
-"   SELECT pg_catalog.quote_ident(pg_catalog.unnest(enumvals)) AS name "\
+"   SELECT pg_catalog.unnest(enumvals) AS name "\
 "     FROM pg_catalog.pg_settings "\
 "    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
 "    UNION ALL " \
@@ -1022,18 +1023,18 @@ Query_for_index_of_table \

 /* the silly-looking length condition is just to eat up the current word */
 #define Query_for_partition_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
 " WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
 "       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"\
 "       and c2.relispartition = 'true'"

 #define Query_for_list_of_cursors \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1043,18 +1044,18 @@ Query_for_index_of_table \

 static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(pubname) "
+        " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pg_catalog.quote_ident(pubname),1,%d)='%s'"
+        "  WHERE substring(pubname,1,%d)='%s'"
     },
     {0, NULL}
 };

 static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(s.subname) "
+        " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(pg_catalog.quote_ident(s.subname),1,%d)='%s' "
+        "  WHERE substring(s.subname,1,%d)='%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1092,7 +1093,7 @@ static const pgsql_thing_t words_after_create[] = {
      * to be used only by pg_dump.
      */
     {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT pg_catalog.quote_ident(conname) FROM pg_catalog.pg_conversion WHERE
substring(pg_catalog.quote_ident(conname),1,%d)='%s'"},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
     {"DATABASE", Query_for_list_of_databases},
     {"DEFAULT PRIVILEGES", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, THING_NO_SHOW},
@@ -1117,7 +1118,7 @@ static const pgsql_thing_t words_after_create[] = {
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
     {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, THING_NO_CREATE},
-    {"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE
substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
@@ -1133,7 +1134,7 @@ static const pgsql_thing_t words_after_create[] = {
                                                                          * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
     {"TRANSFORM", NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT pg_catalog.quote_ident(tgname) FROM pg_catalog.pg_trigger WHERE
substring(pg_catalog.quote_ident(tgname),1,%d)='%s'AND NOT tgisinternal"}, 
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
     {"UNIQUE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNIQUE
                                                                      * INDEX ... */
@@ -1195,11 +1196,13 @@ 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);
 static char *complete_from_query(const char *text, int state);
+static char *complete_from_query_verbatim(const char *text, int state);
 static char *complete_from_versioned_query(const char *text, int state);
 static char *complete_from_schema_query(const char *text, int state);
 static char *complete_from_versioned_schema_query(const char *text, int state);
 static char *_complete_from_query(const char *simple_query,
                                   const SchemaQuery *schema_query,
+                                  bool verbatim,
                                   const char *text, int state);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
@@ -1212,6 +1215,12 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static void parse_identifier(const char *ident,
+                             char **schemaname, char **objectname,
+                             bool *schemaquoted, bool *objectquoted);
+static char *requote_identifier(const char *schemaname, const char *objectname,
+                                bool quote_schema, bool quote_object);
+static bool identifier_needs_quotes(const char *ident);
 static PGresult *exec_query(const char *query);

 static char **get_previous_words(int point, char **buffer, int *nwords);
@@ -1651,7 +1660,7 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE */
     else if (Matches("ALTER", "TABLE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+                                   "UNION SELECT 'ALL IN TABLESPACE', NULL, true");

     /* ALTER something */
     else if (Matches("ALTER"))
@@ -1812,7 +1821,7 @@ psql_completion(const char *text, int start, int end)
     /* ALTER INDEX */
     else if (Matches("ALTER", "INDEX"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+                                   "UNION SELECT 'ALL IN TABLESPACE', NULL, true");
     /* ALTER INDEX <name> */
     else if (Matches("ALTER", "INDEX", MatchAny))
         COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET",
@@ -1879,7 +1888,7 @@ psql_completion(const char *text, int start, int end)
     /* ALTER MATERIALIZED VIEW */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+                                   "UNION SELECT 'ALL IN TABLESPACE', NULL, true");

     /* ALTER USER,ROLE <name> */
     else if (Matches("ALTER", "USER|ROLE", MatchAny) &&
@@ -2164,13 +2173,13 @@ psql_completion(const char *text, int start, int end)
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
     /* ALTER TABLE xxx NO */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO"))
         COMPLETE_WITH("FORCE ROW LEVEL SECURITY", "INHERIT");
     /* ALTER TABLE xxx NO INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO", "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
     /* ALTER TABLE xxx DISABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE"))
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
@@ -2298,7 +2307,7 @@ psql_completion(const char *text, int start, int end)
      * tables.
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("ATTACH", "PARTITION", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2386,7 +2395,7 @@ psql_completion(const char *text, int start, int end)
  */
     else if (Matches("ANALYZE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables,
-                                   " UNION SELECT 'VERBOSE'");
+                                   " UNION SELECT 'VERBOSE', NULL, true");
     else if (HeadMatches("ANALYZE", "(*") &&
              !HeadMatches("ANALYZE", "(*)"))
     {
@@ -2434,7 +2443,8 @@ psql_completion(const char *text, int start, int end)
                             " UNION SELECT 'ALL'");
 /* CLUSTER */
     else if (Matches("CLUSTER"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, "UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables,
+                                   "UNION SELECT 'VERBOSE', NULL, true");
     else if (Matches("CLUSTER", "VERBOSE") ||
              Matches("CLUSTER", "(*)"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, NULL);
@@ -2558,7 +2568,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("COPY|\\copy"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION ALL SELECT '('");
+                                   " UNION ALL SELECT '(', NULL, true");
     /* Complete COPY ( with legal query commands */
     else if (Matches("COPY|\\copy", "("))
         COMPLETE_WITH("SELECT", "TABLE", "VALUES", "INSERT INTO", "UPDATE", "DELETE FROM", "WITH");
@@ -2686,8 +2696,8 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'"
-                                   " UNION SELECT 'CONCURRENTLY'");
+                                   " UNION SELECT 'ON', NULL, true"
+                                   " UNION SELECT 'CONCURRENTLY', NULL, true");

     /*
      * Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of relations
@@ -2703,7 +2713,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'");
+                                   " UNION SELECT 'ON', NULL, true");
     /* Complete CREATE|UNIQUE INDEX [CONCURRENTLY] <sth> with "ON" */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", MatchAny) ||
              TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY", MatchAny))
@@ -2912,7 +2922,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RANGE (", "LIST (", "HASH (");
     /* If we have xxx PARTITION OF, provide a list of partitioned tables */
     else if (TailMatches("PARTITION", "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, NULL);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("PARTITION", "OF", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -3359,7 +3369,7 @@ psql_completion(const char *text, int start, int end)
     /* DROP INDEX */
     else if (Matches("DROP", "INDEX"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+                                   " UNION SELECT 'CONCURRENTLY', NULL, true");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
     else if (Matches("DROP", "INDEX", MatchAny))
@@ -3618,25 +3628,25 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
         else
             COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables,
-                                       " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
-                                       " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
-                                       " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
-                                       " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
-                                       " UNION SELECT 'ALL TABLES IN SCHEMA'"
-                                       " UNION SELECT 'DATABASE'"
-                                       " UNION SELECT 'DOMAIN'"
-                                       " UNION SELECT 'FOREIGN DATA WRAPPER'"
-                                       " UNION SELECT 'FOREIGN SERVER'"
-                                       " UNION SELECT 'FUNCTION'"
-                                       " UNION SELECT 'LANGUAGE'"
-                                       " UNION SELECT 'LARGE OBJECT'"
-                                       " UNION SELECT 'PROCEDURE'"
-                                       " UNION SELECT 'ROUTINE'"
-                                       " UNION SELECT 'SCHEMA'"
-                                       " UNION SELECT 'SEQUENCE'"
-                                       " UNION SELECT 'TABLE'"
-                                       " UNION SELECT 'TABLESPACE'"
-                                       " UNION SELECT 'TYPE'");
+                                       " UNION SELECT 'ALL FUNCTIONS IN SCHEMA', NULL, true"
+                                       " UNION SELECT 'ALL PROCEDURES IN SCHEMA', NULL, true"
+                                       " UNION SELECT 'ALL ROUTINES IN SCHEMA', NULL, true"
+                                       " UNION SELECT 'ALL SEQUENCES IN SCHEMA', NULL, true"
+                                       " UNION SELECT 'ALL TABLES IN SCHEMA', NULL, true"
+                                       " UNION SELECT 'DATABASE', NULL, true"
+                                       " UNION SELECT 'DOMAIN', NULL, true"
+                                       " UNION SELECT 'FOREIGN DATA WRAPPER', NULL, true"
+                                       " UNION SELECT 'FOREIGN SERVER', NULL, true"
+                                       " UNION SELECT 'FUNCTION', NULL, true"
+                                       " UNION SELECT 'LANGUAGE', NULL, true"
+                                       " UNION SELECT 'LARGE OBJECT', NULL, true"
+                                       " UNION SELECT 'PROCEDURE', NULL, true"
+                                       " UNION SELECT 'ROUTINE', NULL, true"
+                                       " UNION SELECT 'SCHEMA', NULL, true"
+                                       " UNION SELECT 'SEQUENCE', NULL, true"
+                                       " UNION SELECT 'TABLE', NULL, true"
+                                       " UNION SELECT 'TABLESPACE', NULL, true"
+                                       " UNION SELECT 'TYPE', NULL, true");
     }
     else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
         COMPLETE_WITH("FUNCTIONS IN SCHEMA",
@@ -3789,11 +3799,11 @@ psql_completion(const char *text, int start, int end)
     /* Complete LOCK [TABLE] [ONLY] with a list of tables */
     else if (Matches("LOCK"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+                                   " UNION SELECT 'TABLE', NULL, true"
+                                   " UNION SELECT 'ONLY', NULL, true");
     else if (Matches("LOCK", "TABLE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'ONLY'");
+                                   " UNION SELECT 'ONLY', NULL, true");
     else if (Matches("LOCK", "TABLE", "ONLY") || Matches("LOCK", "ONLY"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
     /* For the following, handle the case of a single table only for now */
@@ -3831,7 +3841,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s'"); 
+        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -3875,7 +3885,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("VIEW");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   " UNION SELECT 'CONCURRENTLY'");
+                                   " UNION SELECT 'CONCURRENTLY', NULL, true");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", MatchAny))
@@ -3898,15 +3908,15 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("REINDEX", "TABLE") ||
              Matches("REINDEX", "(*)", "TABLE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables,
-                                   " UNION SELECT 'CONCURRENTLY'");
+                                   " UNION SELECT 'CONCURRENTLY', NULL, true");
     else if (Matches("REINDEX", "INDEX") ||
              Matches("REINDEX", "(*)", "INDEX"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+                                   " UNION SELECT 'CONCURRENTLY', NULL, true");
     else if (Matches("REINDEX", "SCHEMA") ||
              Matches("REINDEX", "(*)", "SCHEMA"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " UNION SELECT 'CONCURRENTLY'");
+                            " UNION SELECT 'CONCURRENTLY', NULL, true");
     else if (Matches("REINDEX", "SYSTEM|DATABASE") ||
              Matches("REINDEX", "(*)", "SYSTEM|DATABASE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_databases
@@ -3997,7 +4007,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ONLY", "WRITE");
     /* SET CONSTRAINTS */
     else if (Matches("SET", "CONSTRAINTS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema, "UNION SELECT 'ALL'");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema,
+                                   "UNION SELECT 'ALL', NULL, true");
     /* Complete SET CONSTRAINTS <foo> with DEFERRED|IMMEDIATE */
     else if (Matches("SET", "CONSTRAINTS", MatchAny))
         COMPLETE_WITH("DEFERRED", "IMMEDIATE");
@@ -4091,11 +4102,11 @@ psql_completion(const char *text, int start, int end)
 /* TRUNCATE */
     else if (Matches("TRUNCATE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+                                   " UNION SELECT 'TABLE', NULL, true"
+                                   " UNION SELECT 'ONLY', NULL, true");
     else if (Matches("TRUNCATE", "TABLE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'ONLY'");
+                                   " UNION SELECT 'ONLY', NULL, true");
     else if (HeadMatches("TRUNCATE") && TailMatches("ONLY"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables, NULL);
     else if (Matches("TRUNCATE", MatchAny) ||
@@ -4107,7 +4118,7 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s' UNION SELECT '*'"); 
+        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'UNION SELECT '*'"); 

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
@@ -4145,25 +4156,25 @@ psql_completion(const char *text, int start, int end)
  */
     else if (Matches("VACUUM"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FULL'"
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+                                   " UNION SELECT 'FULL', NULL, true"
+                                   " UNION SELECT 'FREEZE', NULL, true"
+                                   " UNION SELECT 'ANALYZE', NULL, true"
+                                   " UNION SELECT 'VERBOSE', NULL, true");
     else if (Matches("VACUUM", "FULL"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+                                   " UNION SELECT 'FREEZE', NULL, true"
+                                   " UNION SELECT 'ANALYZE', NULL, true"
+                                   " UNION SELECT 'VERBOSE', NULL, true");
     else if (Matches("VACUUM", "FREEZE") ||
              Matches("VACUUM", "FULL", "FREEZE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'VERBOSE'"
-                                   " UNION SELECT 'ANALYZE'");
+                                   " UNION SELECT 'VERBOSE', NULL, true"
+                                   " UNION SELECT 'ANALYZE', NULL, true");
     else if (Matches("VACUUM", "VERBOSE") ||
              Matches("VACUUM", "FULL|FREEZE", "VERBOSE") ||
              Matches("VACUUM", "FULL", "FREEZE", "VERBOSE"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'ANALYZE'");
+                                   " UNION SELECT 'ANALYZE', NULL, true");
     else if (HeadMatches("VACUUM", "(*") &&
              !HeadMatches("VACUUM", "(*)"))
     {
@@ -4551,7 +4562,14 @@ static char *
 complete_from_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, NULL, text, state);
+    return _complete_from_query(completion_charp, NULL, false, text, state);
+}
+
+static char *
+complete_from_query_verbatim(const char *text, int state)
+{
+    /* query is assumed to work for any server version */
+    return _complete_from_query(completion_charp, NULL, true, text, state);
 }

 static char *
@@ -4566,7 +4584,7 @@ complete_from_versioned_query(const char *text, int state)
     if (vquery->query == NULL)
         return NULL;

-    return _complete_from_query(vquery->query, NULL, text, state);
+    return _complete_from_query(vquery->query, NULL, false, text, state);
 }

 static char *
@@ -4574,7 +4592,7 @@ complete_from_schema_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
     return _complete_from_query(completion_charp, completion_squery,
-                                text, state);
+                                false, text, state);
 }

 static char *
@@ -4600,7 +4618,7 @@ complete_from_versioned_schema_query(const char *text, int state)
     }

     return _complete_from_query(vquery ? vquery->query : NULL,
-                                squery, text, state);
+                                squery, false, text, state);
 }


@@ -4618,28 +4636,40 @@ complete_from_versioned_schema_query(const char *text, int state)
  * completion_info_charp2.
  *
  * 2. A schema query used for completion of both schema and relation names.
- * These are more complex and must contain in the following order:
- * %d %s %d %s %d %s %s %d %s
- * where %d is the string length of the text and %s the text itself.
+ * This is represented by a SchemaQuery object; see that typedef for details.
+ *
+ * In either case, the query can return up to three columns: an object name
+ * column, a schema name column, and a boolean column which if TRUE indicates
+ * that the object-name column should be returned verbatim.  If the boolean
+ * column is false or omitted, we will combine and appropriately quote the
+ * schema and object names.
  *
  * If both simple_query and schema_query are non-NULL, then we construct
  * a schema query and append the (uninterpreted) string simple_query to it.
+ * The simple query must return all three columns in this case.
  *
  * It is assumed that strings should be escaped to become SQL literals
  * (that is, what is in the query is actually ... '%s' ...)
  *
  * See top of file for examples of both kinds of query.
  *
+ * If "verbatim" is true, then we use the given text as-is to match the
+ * query results; otherwise we parse it as a possibly-qualified identifier,
+ * and reconstruct suitable quoting afterward.  This also changes the
+ * default assumption about whether the results are verbatim.
+ *
  * "text" and "state" are supplied by readline.
  */
 static char *
 _complete_from_query(const char *simple_query,
                      const SchemaQuery *schema_query,
+                     bool verbatim,
                      const char *text, int state)
 {
-    static int    list_index,
-                byte_length;
+    static int    list_index;
     static PGresult *result = NULL;
+    static bool schemaquoted;
+    static bool objectquoted;

     /*
      * If this is the first time for this completion, we fetch a list of our
@@ -4648,31 +4678,56 @@ _complete_from_query(const char *simple_query,
     if (state == 0)
     {
         PQExpBufferData query_buffer;
-        char       *e_text;
+        char       *schemaname;
+        char       *objectname;
+        int            schema_length = 0;
+        int            object_length = 0;
+        char       *e_schemaname;
+        char       *e_objectname;
         char       *e_info_charp;
         char       *e_info_charp2;
-        const char *pstr = text;
-        int            char_length = 0;

+        /* Reset static state, ensuring no memory leaks */
         list_index = 0;
-        byte_length = strlen(text);
+        PQclear(result);
+        result = NULL;
+
+        if (verbatim)
+        {
+            objectname = pg_strdup(text);
+            schemaname = NULL;
+        }
+        else
+        {
+            /* Parse text, splitting into schema and object name if needed */
+            parse_identifier(text,
+                             &schemaname, &objectname,
+                             &schemaquoted, &objectquoted);
+        }

         /*
-         * Count length as number of characters (not bytes), for passing to
+         * Count lengths as number of characters (not bytes), for passing to
          * substring
          */
-        while (*pstr)
+        if (schemaname)
         {
-            char_length++;
-            pstr += PQmblenBounded(pstr, pset.encoding);
+            for (const char *p = schemaname;
+                 *p;
+                 p += PQmblenBounded(p, pset.encoding))
+                schema_length++;
         }
-
-        /* Free any prior result */
-        PQclear(result);
-        result = NULL;
+        for (const char *p = objectname;
+             *p;
+             p += PQmblenBounded(p, pset.encoding))
+            object_length++;

         /* Set up suitably-escaped copies of textual inputs */
-        e_text = escape_string(text);
+        if (schemaname)
+            e_schemaname = escape_string(schemaname);
+        else
+            e_schemaname = NULL;
+
+        e_objectname = escape_string(objectname);

         if (completion_info_charp)
             e_info_charp = escape_string(completion_info_charp);
@@ -4694,8 +4749,14 @@ _complete_from_query(const char *simple_query,
             if (qualresult == NULL)
                 qualresult = schema_query->result;

+            /*
+             * We issue different queries depending on whether the input is
+             * qualified or not.
+             */
+            if (schemaname == NULL)
+            {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s FROM %s WHERE ",
+            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text, false FROM %s WHERE ",
                               schema_query->result,
                               schema_query->catname);
             if (schema_query->selcondition)
@@ -4703,7 +4764,7 @@ _complete_from_query(const char *simple_query,
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
-                              char_length, e_text);
+                              object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);

@@ -4715,35 +4776,32 @@ _complete_from_query(const char *simple_query,
              */
             if (strcmp(schema_query->catname,
                        "pg_catalog.pg_class c") == 0 &&
-                strncmp(text, "pg_", 3) != 0)
+                strncmp(objectname, "pg_", 3) != 0)
             {
                 appendPQExpBufferStr(&query_buffer,
                                      " AND c.relnamespace <> (SELECT oid FROM"
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

-            /*
-             * Add in matching schema names, but only if there is more than
-             * one potential match among schema names.
-             */
+            /* Add in schema names matching the input-so-far */
             appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' "
+                              "SELECT NULL::pg_catalog.text, n.nspname, false "
                               "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d)='%s'",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) > 1",
-                              char_length, e_text);
+                              "WHERE substring(n.nspname,1,%d)='%s'",
+                              object_length, e_objectname);

             /*
-             * Add in matching qualified names, but only if there is exactly
-             * one schema matching the input-so-far.
+             * Likewise, suppress system schemas unless the input-so-far
+             * begins with "pg_".
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' || %s "
+            if (strncmp(objectname, "pg_", 3) != 0)
+                appendPQExpBufferStr(&query_buffer,
+                                     " AND n.nspname NOT LIKE 'pg\\_%'");
+            }
+            else
+            {
+            /* Input is qualified, so produce only qualified names */
+            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname, false "
                               "FROM %s, pg_catalog.pg_namespace n "
                               "WHERE %s = n.oid AND ",
                               qualresult,
@@ -4752,24 +4810,12 @@ _complete_from_query(const char *simple_query,
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(pg_catalog.quote_ident(n.nspname) || '.' || %s,1,%d)='%s'",
+            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
                               qualresult,
-                              char_length, e_text);
-
-            /*
-             * This condition exploits the single-matching-schema rule to
-             * speed up the query
-             */
-            appendPQExpBuffer(&query_buffer,
-                              " AND substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(n.nspname))+1)",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1",
-                              char_length, e_text);
+                              object_length, e_objectname);
+            appendPQExpBuffer(&query_buffer, "substring(n.nspname,1,%d)='%s'",
+                              schema_length, e_schemaname);
+            }

             /* If an addon query was provided, use it */
             if (simple_query)
@@ -4780,7 +4826,7 @@ _complete_from_query(const char *simple_query,
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              char_length, e_text,
+                              object_length, e_objectname,
                               e_info_charp, e_info_charp,
                               e_info_charp2, e_info_charp2);
         }
@@ -4792,25 +4838,49 @@ _complete_from_query(const char *simple_query,
         result = exec_query(query_buffer.data);

         termPQExpBuffer(&query_buffer);
-        free(e_text);
+        if (e_schemaname)
+            free(e_schemaname);
+        free(e_objectname);
         if (e_info_charp)
             free(e_info_charp);
         if (e_info_charp2)
             free(e_info_charp2);
     }

-    /* Find something that matches */
-    if (result && PQresultStatus(result) == PGRES_TUPLES_OK)
+    /* Return the next result, if there is one */
+    if (result && PQresultStatus(result) == PGRES_TUPLES_OK &&
+        list_index < PQntuples(result))
     {
-        const char *item;
+        const char *item = NULL;
+        const char *nsp = NULL;
+        bool        thisverbatim = verbatim;
+
+        if (!PQgetisnull(result, list_index, 0))
+            item = PQgetvalue(result, list_index, 0);
+        if (PQnfields(result) > 1 &&
+            !PQgetisnull(result, list_index, 1))
+            nsp = PQgetvalue(result, list_index, 1);
+        if (PQnfields(result) > 2)
+            thisverbatim = (PQgetvalue(result, list_index, 2)[0] == 't');
+        list_index++;
+
+        if (thisverbatim)
+            return pg_strdup(item);

-        while (list_index < PQntuples(result) &&
-               (item = PQgetvalue(result, list_index++, 0)))
-            if (pg_strncasecmp(text, item, byte_length) == 0)
-                return pg_strdup(item);
+        /*
+         * Hack: if we're returning one single schema name, don't let Readline
+         * add a space after it.  Otherwise it'll stop being part of the
+         * completion subject text, which is not what we want.
+         */
+#ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
+        if (item == NULL && nsp != NULL && PQntuples(result) == 1)
+            rl_completion_append_character = '\0';
+#endif
+
+        return requote_identifier(nsp, item, schemaquoted, objectquoted);
     }

-    /* If nothing matches, free the db structure and return null */
+    /* If nothing (else) matches, free the db structure and return null */
     PQclear(result);
     result = NULL;
     return NULL;
@@ -5144,6 +5214,199 @@ escape_string(const char *text)
 }


+/*
+ * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
+ *
+ * This involves splitting off the schema name if present, de-quoting,
+ * and downcasing any unquoted text.  We are a bit laxer than the backend
+ * in that we allow just portions of a name to be quoted --- that's because
+ * psql metacommands have traditionally behaved that way.
+ *
+ * Outputs are a malloc'd schema name (NULL if none), malloc'd object name,
+ * and booleans telling whether any part of the schema and object name was
+ * double-quoted.
+ */
+static void
+parse_identifier(const char *ident,
+                 char **schemaname, char **objectname,
+                 bool *schemaquoted, bool *objectquoted)
+{
+    size_t        buflen = strlen(ident) + 1;
+    bool        enc_is_single_byte = (pg_encoding_max_length(pset.encoding) == 1);
+    char       *sname;
+    char       *oname;
+    char       *optr;
+    bool        inquotes;
+
+    /* Initialize, making a certainly-large-enough output buffer */
+    sname = NULL;
+    oname = pg_malloc(buflen);
+    *schemaquoted = *objectquoted = false;
+    /* Scan */
+    optr = oname;
+    inquotes = false;
+    while (*ident)
+    {
+        unsigned char ch = (unsigned char) *ident++;
+
+        if (ch == '"')
+        {
+            if (inquotes && *ident == '"')
+            {
+                /* two quote marks within a quoted identifier = emit quote */
+                *optr++ = '"';
+                ident++;
+            }
+            else
+            {
+                inquotes = !inquotes;
+                *objectquoted = true;
+            }
+        }
+        else if (ch == '.' && !inquotes)
+        {
+            /* Found a schema name, transfer it to sname / *schemaquoted */
+            *optr = '\0';
+            free(sname);        /* drop any catalog name */
+            sname = oname;
+            oname = pg_malloc(buflen);
+            optr = oname;
+            *schemaquoted = *objectquoted;
+            *objectquoted = false;
+        }
+        else
+        {
+            if (!inquotes)
+            {
+                /*
+                 * This downcasing transformation should match the backend's
+                 * downcase_identifier() as best we can.  We do not know the
+                 * backend's locale, though, so it's necessarily approximate.
+                 * We assume that psql is operating in the same locale and
+                 * encoding as the backend.
+                 */
+                if (ch >= 'A' && ch <= 'Z')
+                    ch += 'a' - 'A';
+                else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch))
+                    ch = tolower(ch);
+            }
+            *optr++ = (char) ch;
+        }
+    }
+
+    *optr = '\0';
+    *schemaname = sname;
+    *objectname = oname;
+}
+
+
+/*
+ * requote_identifier - Reconstruct a possibly-schema-qualified SQL identifier.
+ *
+ * Build a malloc'd string containing the identifier, with quoting applied
+ * as necessary.  This is more or less the inverse of parse_identifier;
+ * in particular, if an input component was quoted, we'll quote the output
+ * even when that isn't strictly required.
+ *
+ * Unlike parse_identifier, we handle the case where a schema and no
+ * object name is provided, producing just "schema.".
+ */
+static char *
+requote_identifier(const char *schemaname, const char *objectname,
+                   bool quote_schema, bool quote_object)
+{
+    char       *result;
+    size_t        buflen = 1;        /* count the trailing \0 */
+    char       *ptr;
+
+    /*
+     * We could use PQescapeIdentifier for some of this, but not all, and it
+     * adds more notational cruft than it seems worth.
+     */
+    if (schemaname)
+    {
+        buflen += strlen(schemaname) + 1;    /* +1 for the dot */
+        if (!quote_schema)
+            quote_schema = identifier_needs_quotes(schemaname);
+        if (quote_schema)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = schemaname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    if (objectname)
+    {
+        buflen += strlen(objectname);
+        if (!quote_object)
+            quote_object = identifier_needs_quotes(objectname);
+        if (quote_object)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = objectname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    result = pg_malloc(buflen);
+    ptr = result;
+    if (schemaname)
+    {
+        if (quote_schema)
+            *ptr++ = '"';
+        for (const char *p = schemaname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_schema)
+            *ptr++ = '"';
+        *ptr++ = '.';
+    }
+    if (objectname)
+    {
+        if (quote_object)
+            *ptr++ = '"';
+        for (const char *p = objectname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_object)
+            *ptr++ = '"';
+    }
+    *ptr = '\0';
+    return result;
+}
+
+
+/*
+ * Detect whether an identifier must be double-quoted.
+ *
+ * Note we'll quote anything that's not ASCII; the backend's quote_ident()
+ * does the same.
+ */
+static bool
+identifier_needs_quotes(const char *ident)
+{
+    if (!((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'))
+        return true;
+    if (strspn(ident, "abcdefghijklmnopqrstuvwxyz0123456789_") != strlen(ident))
+        return true;
+
+    /* XXX is it worth checking for SQL reserved words? */
+
+    return false;
+}
+
+
 /*
  * Execute a query and report any errors. This should be the preferred way of
  * talking to the database in this file.

RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Tuesday, January 25, 2022 6:44 PM, Julien Rouhaud <rjuju123@gmail.com> wrote:
> Thanks for updating the patch.  When you do so, please check and update the
> commitfest entry accordingly to make sure that people knows it's ready for
> review.  I'm switching the entry to Needs Review.
>

Thanks for your reminder. I'll watch out the status change as you suggested.

Regards,
Tang



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
I wrote:
> I spent some time contemplating my navel about the concerns I raised
> upthread about double-quoted identifiers.  I concluded that the reason
> things don't work well in that area is that we're trying to get all the
> work done by applying quote_ident() on the backend side and then
> ignoring quoting considerations in tab-complete itself.  That sort of
> works, but not terribly well.  The currently proposed patch is sticking
> a toe into the water of dealing with quoting/downcasing in tab-complete,
> but we need to go a lot further.  I propose that we ought to drop the
> use of quote_ident() in the tab completion queries altogether, instead
> having the backend return names as-is, and doing all the dequoting and
> requoting work in tab-complete.

Here's a fleshed-out patch series for this idea.

0001 below is a more evolved version of my previous WIP patch.
The main thing that's changed is that I found that it no longer
works to handle extra keywords (such as adding COLUMN to the list
of attribute names after ALTER TABLE tab RENAME) via tab-complete's
traditional method of sticking on "UNION SELECT 'foo'".  That's
because such a result row looks like something that needs to be
double-quoted, which of course makes the completion incorrect.
So I've created a side-channel whereby _complete_from_query() can
return some verbatim keywords alongside the actual query results.
(I think this is a good thing anyway, because the UNION method is
incredibly wasteful of server cycles.  Yeah, I know that these
queries only need to run at human speed, but if your server is
heavily loaded you might still not appreciate the extra cycles.)

Having done that, I solved the format_type problem by just dropping
the use of format_type altogether, and returning only the normal
pg_type.typname entries.  The only cases where format_type did
anything useful for us are the small number of built-in types where
it substitutes a SQL-mandated name, and we can treat those like
keywords (as indeed they are).  The list of such types changes
seldom enough that I don't think it's a huge maintenance burden to
have one more place that knows about them.

BTW, I was amused to notice that many of the format_type special cases
don't actually work as completions, and never have.  For example,
if we return both "timestamp with time zone" and "timestamp without
time zone", readline can complete as far as "timestamp with", but
further completion fails because "timestamp" is now seen as a previous
word that's not part of what's to be completed.  So I've dropped those
cases from the keyword list.  Maybe somebody will get interested in
figuring a way to make that work, but IMO the cost/benefit ratio for
such effort would be pretty bad.

Incidentally, I found that some of the completion queries were
intentionally ignoring the given prefix text, with stuff like

/* the silly-looking length condition is just to eat up the current word */
" WHERE ... (%d = pg_catalog.length('%s'))"

I'm not sure why we ever thought that was a good idea, but it
definitely doesn't work anymore, since I removed the filtering
that _complete_from_query() used to do on the query results.
It's now incumbent on the queries to only return valid matches,
so I replaced all instances of this pattern with the regular
substring() checks, or even added a substring() check in a
couple of queries where there was nothing at all.

0001 takes care of quoting and case-folding issues for the actual
subject name of a completion operation, but there's more to do.
A lot of queries have to reference a previously-entered name
(for example, ALTER TABLE tab1 DROP COLUMN <TAB> has to find the
column names of table tab1), and we had variously shoddy code
for dealing with those names.  Only a few queries even attempted
to handle schema-qualified names, and none at all of them would
downcase unquoted names.  So 0002 tries to fix that up, using
the same code to parse/downcase/de-quote the name as we would
use if it were the subject text.

It didn't take long to find that the existing methods for this
were incredibly tedious, requiring near-duplicate queries
depending on whether the previous name was schema-qualified or
not.  So I've extended the SchemaQuery mechanism to support
adding qualifications based on an additional name, and now
we use that wherever we need a possibly-schema-qualified
previous name.

A couple of the existing queries of this sort used WHERE oid
IN (sub-SELECT), which I didn't see a great way to jam into
the SchemaQuery mechanism.  What I've done here is to convert
those semijoins into plain joins, which might yield multiple
instances of wanted names, and then stick DISTINCT onto the
queries.  It's not very pretty, but it works fine.

In 0001 and 0002, I left the core of _complete_from_query()
un-reindented, in hopes of making the actual code changes
more readily reviewable.  0003 is just an application of pgindent
to fix that up and make the finished code legible again.

Finally, 0004 adds some test cases.  I'm not too confident about
how portable these will be, but I don't think they are making any
assumptions the existing tests didn't make already.  They do pass
for me on Linux (readline 7.0) and macOS (Apple's libedit).

This is sufficiently invasive to tab-complete.c that I'd like to
get it pushed fairly soon, before that code changes under me.
Thoughts?

            regards, tom lane

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 502b5c5751..27a0ecfaf4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_class_d.h"
 #include "common.h"
 #include "libpq-fe.h"
+#include "mb/pg_wchar.h"
 #include "pqexpbuffer.h"
 #include "settings.h"
 #include "stringutils.h"
@@ -148,16 +149,17 @@ typedef struct SchemaQuery
     const char *namespace;

     /*
-     * Result --- the appropriately-quoted name to return, in the case of an
-     * unqualified name.  For example, "pg_catalog.quote_ident(c.relname)".
+     * Result --- the (unquoted) name to return, in the case of an unqualified
+     * name.  For example, "c.relname".
      */
     const char *result;

     /*
-     * In some cases a different result must be used for qualified names.
-     * Enter that here, or write NULL if result can be used.
+     * Additional literal strings (usually keywords) to be offered along with
+     * the query results.  Provide a NULL-terminated array of constant
+     * strings, or NULL if none.
      */
-    const char *qualresult;
+    const char *const *keywords;
 } SchemaQuery;


@@ -179,6 +181,7 @@ static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
 static bool completion_case_sensitive;    /* completion is case sensitive */
+static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */

 /*
@@ -190,36 +193,85 @@ static bool completion_force_quote; /* true to force-quote filenames */
  *      We support both simple and versioned schema queries.
  * 3) The items from a null-pointer-terminated list (with or without
  *      case-sensitive comparison); if the list is constant you can build it
- *      with COMPLETE_WITH() or COMPLETE_WITH_CS().
+ *      with COMPLETE_WITH() or COMPLETE_WITH_CS().  The QUERY_LIST and
+ *      QUERY_PLUS forms combine such literal lists with a query result.
  * 4) The list of attributes of the given table (possibly schema-qualified).
  * 5) The list of arguments to the given function (possibly schema-qualified).
  */
 #define COMPLETE_WITH_QUERY(query) \
+    COMPLETE_WITH_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_QUERY_LIST(query, list) \
 do { \
     completion_charp = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
+    matches = rl_completion_matches(text, complete_from_query); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_VERBATIM(query) \
+do { \
+    completion_charp = query; \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

 #define COMPLETE_WITH_VERSIONED_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list) \
 do { \
     completion_vquery = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_versioned_query); \
 } while (0)

-#define COMPLETE_WITH_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_VERSIONED_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = &(query); \
-    completion_charp = addon; \
+    completion_charpp = list; \
     matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

-#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = query; \
-    completion_vquery = addon; \
+    completion_charpp = list; \
     matches = rl_completion_matches(text, complete_from_versioned_schema_query); \
 } while (0)

+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
 /*
  * Caution: COMPLETE_WITH_CONST is not for general-purpose use; you probably
  * want COMPLETE_WITH() with one element, instead.
@@ -253,7 +305,10 @@ do { \
     COMPLETE_WITH_LIST_CS(list); \
 } while (0)

-#define COMPLETE_WITH_ATTR(relation, addon) \
+#define COMPLETE_WITH_ATTR(relation) \
+    COMPLETE_WITH_ATTR_LIST(relation, NULL)
+
+#define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
     char   *_completion_schema; \
     char   *_completion_table; \
@@ -266,18 +321,26 @@ do { \
                                 false, false, pset.encoding); \
     if (_completion_table == NULL) \
     { \
-        completion_charp = Query_for_list_of_attributes  addon; \
+        completion_charp = Query_for_list_of_attributes; \
         completion_info_charp = relation; \
     } \
     else \
     { \
-        completion_charp = Query_for_list_of_attributes_with_schema  addon; \
+        completion_charp = Query_for_list_of_attributes_with_schema; \
         completion_info_charp = _completion_table; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

+#define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_ATTR_LIST(relation, list); \
+} while (0)
+
 /*
  * libedit will typically include the literal's leading single quote in
  * "text", while readline will not.  Adapt our offered strings to fit.
@@ -315,6 +378,8 @@ do { \
         completion_info_charp = _completion_type; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -340,6 +405,8 @@ do { \
         completion_info_charp = _completion_function; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -357,17 +424,45 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
         .selcondition = "p.prokind = 'a'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .selcondition = "p.proisagg",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

+static const char *const Keywords_for_list_of_datatypes[] = {
+    "bigint",
+    "boolean",
+    "character",
+    "double precision",
+    "integer",
+    "real",
+    "smallint",
+
+    /*
+     * Note: currently there's no value in offering the following multiword
+     * type names, because tab completion cannot succeed for them: we can't
+     * disambiguate until somewhere in the second word, at which point we
+     * won't have the first word as context.  ("double precision" does work,
+     * as long as no other type name begins with "double".)  Leave them out to
+     * encourage users to use the PG-specific aliases, which we can complete.
+     */
+#ifdef NOT_USED
+    "bit varying",
+    "character varying",
+    "time with time zone",
+    "time without time zone",
+    "timestamp with time zone",
+    "timestamp without time zone",
+#endif
+    NULL
+};
+
 static const SchemaQuery Query_for_list_of_datatypes = {
     .catname = "pg_catalog.pg_type t",
     /* selcondition --- ignore table rowtypes and array types */
@@ -377,8 +472,8 @@ static const SchemaQuery Query_for_list_of_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
+    .keywords = Keywords_for_list_of_datatypes,
 };

 static const SchemaQuery Query_for_list_of_composite_datatypes = {
@@ -389,8 +484,7 @@ static const SchemaQuery Query_for_list_of_composite_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 static const SchemaQuery Query_for_list_of_domains = {
@@ -398,7 +492,7 @@ static const SchemaQuery Query_for_list_of_domains = {
     .selcondition = "t.typtype = 'd'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 /* Note: this intentionally accepts aggregates as well as plain functions */
@@ -409,13 +503,13 @@ static const SchemaQuery Query_for_list_of_functions[] = {
         .selcondition = "p.prokind != 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

@@ -426,7 +520,7 @@ static const SchemaQuery Query_for_list_of_procedures[] = {
         .selcondition = "p.prokind = 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         /* not supported in older versions */
@@ -438,7 +532,7 @@ static const SchemaQuery Query_for_list_of_routines = {
     .catname = "pg_catalog.pg_proc p",
     .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
     .namespace = "p.pronamespace",
-    .result = "pg_catalog.quote_ident(p.proname)",
+    .result = "p.proname",
 };

 static const SchemaQuery Query_for_list_of_sequences = {
@@ -446,7 +540,7 @@ static const SchemaQuery Query_for_list_of_sequences = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_SEQUENCE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_foreign_tables = {
@@ -454,7 +548,7 @@ static const SchemaQuery Query_for_list_of_foreign_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_tables = {
@@ -464,7 +558,7 @@ static const SchemaQuery Query_for_list_of_tables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_tables = {
@@ -472,7 +566,7 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_views = {
@@ -480,7 +574,7 @@ static const SchemaQuery Query_for_list_of_views = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_matviews = {
@@ -488,7 +582,7 @@ static const SchemaQuery Query_for_list_of_matviews = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_indexes = {
@@ -498,7 +592,7 @@ static const SchemaQuery Query_for_list_of_indexes = {
     CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_indexes = {
@@ -506,7 +600,7 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
     .selcondition = "c.relkind = " CppAsString2(RELKIND_PARTITIONED_INDEX),
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };


@@ -515,7 +609,7 @@ static const SchemaQuery Query_for_list_of_relations = {
     .catname = "pg_catalog.pg_class c",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* partitioned relations */
@@ -525,14 +619,14 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = {
     ", " CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_operator_families = {
     .catname = "pg_catalog.pg_opfamily c",
     .viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)",
     .namespace = "c.opfnamespace",
-    .result = "pg_catalog.quote_ident(c.opfname)",
+    .result = "c.opfname",
 };

 /* Relations supporting INSERT, UPDATE or DELETE */
@@ -545,7 +639,7 @@ static const SchemaQuery Query_for_list_of_updatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting SELECT */
@@ -560,7 +654,7 @@ static const SchemaQuery Query_for_list_of_selectables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting TRUNCATE */
@@ -572,7 +666,7 @@ static const SchemaQuery Query_for_list_of_truncatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting GRANT are currently same as those supporting SELECT */
@@ -588,7 +682,7 @@ static const SchemaQuery Query_for_list_of_analyzables = {
     CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting index creation */
@@ -600,7 +694,7 @@ static const SchemaQuery Query_for_list_of_indexables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /*
@@ -617,7 +711,7 @@ static const SchemaQuery Query_for_list_of_clusterables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
@@ -625,14 +719,14 @@ static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .selcondition = "c.conrelid <> 0",
     .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
-    .result = "pg_catalog.quote_ident(c.conname)",
+    .result = "c.conname",
 };

 static const SchemaQuery Query_for_list_of_statistics = {
     .catname = "pg_catalog.pg_statistic_ext s",
     .viscondition = "pg_catalog.pg_statistics_obj_is_visible(s.oid)",
     .namespace = "s.stxnamespace",
-    .result = "pg_catalog.quote_ident(s.stxname)",
+    .result = "s.stxname",
 };

 static const SchemaQuery Query_for_list_of_collations = {
@@ -640,7 +734,7 @@ static const SchemaQuery Query_for_list_of_collations = {
     .selcondition = "c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))",
     .viscondition = "pg_catalog.pg_collation_is_visible(c.oid)",
     .namespace = "c.collnamespace",
-    .result = "pg_catalog.quote_ident(c.collname)",
+    .result = "c.collname",
 };


@@ -659,13 +753,13 @@ static const SchemaQuery Query_for_list_of_collations = {
  */

 #define Query_for_list_of_attributes \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
 " WHERE c.oid = a.attrelid "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

@@ -676,21 +770,21 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
 "   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

 #define Query_for_list_of_attributes_with_schema \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
 " WHERE c.oid = a.attrelid "\
 "   AND n.oid = c.relnamespace "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_quoted \
@@ -698,7 +792,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
 " WHERE t.oid = e.enumtypid "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -707,7 +801,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  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' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -717,9 +811,9 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_with_schema_unquoted \
@@ -728,24 +822,24 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(enumlabel,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_template_databases \
-"SELECT pg_catalog.quote_ident(d.datname) "\
+"SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(pg_catalog.quote_ident(d.datname),1,%d)='%s' "\
+" WHERE substring(d.datname,1,%d)='%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
-"SELECT pg_catalog.quote_ident(datname) FROM pg_catalog.pg_database "\
-" WHERE substring(pg_catalog.quote_ident(datname),1,%d)='%s'"
+"SELECT datname FROM pg_catalog.pg_database "\
+" WHERE substring(datname,1,%d)='%s'"

 #define Query_for_list_of_tablespaces \
-"SELECT pg_catalog.quote_ident(spcname) FROM pg_catalog.pg_tablespace "\
-" WHERE substring(pg_catalog.quote_ident(spcname),1,%d)='%s'"
+"SELECT spcname FROM pg_catalog.pg_tablespace "\
+" WHERE substring(spcname,1,%d)='%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
@@ -753,287 +847,260 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"

 #define Query_for_list_of_languages \
-"SELECT pg_catalog.quote_ident(lanname) "\
+"SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(pg_catalog.quote_ident(lanname),1,%d)='%s'"
+"   AND substring(lanname,1,%d)='%s'"

 #define Query_for_list_of_schemas \
-"SELECT pg_catalog.quote_ident(nspname) FROM pg_catalog.pg_namespace "\
-" WHERE substring(pg_catalog.quote_ident(nspname),1,%d)='%s'"
+"SELECT nspname FROM pg_catalog.pg_namespace "\
+" WHERE substring(nspname,1,%d)='%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context != 'internal' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context IN ('user', 'superuser') "\
-"  UNION ALL SELECT 'constraints' "\
-"  UNION ALL SELECT 'transaction' "\
-"  UNION ALL SELECT 'session' "\
-"  UNION ALL SELECT 'role' "\
-"  UNION ALL SELECT 'tablespace' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
-"  UNION ALL SELECT 'session authorization' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
+" SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"
+"  WHERE substring(rolname,1,%d)='%s'"

-#define Query_for_list_of_grant_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
-"   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"\
-" UNION ALL SELECT 'PUBLIC'"\
-" UNION ALL SELECT 'CURRENT_ROLE'"\
-" UNION ALL SELECT 'CURRENT_USER'"\
-" UNION ALL SELECT 'SESSION_USER'"
+/* add these to Query_for_list_of_roles in GRANT contexts */
+#define Keywords_for_list_of_grant_roles \
+"PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_index_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
 " WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"

 #define Query_for_unique_index_of_table \
 Query_for_index_of_table \
 "       and i.indisunique"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table_not_validated \
-"SELECT pg_catalog.quote_ident(conname) "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)" \
+Query_for_constraint_of_table \
 "       and not con.convalidated"

 #define Query_for_all_table_constraints \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
-" WHERE c.conrelid <> 0 "
+" WHERE c.conrelid <> 0 "\
+"       and substring(conname,1,%d)='%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_type \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(t.typname)='%s'"\
+" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
+"       and t.typname='%s'"\
 "       and pg_catalog.pg_type_is_visible(t.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_constraint \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE pg_catalog.quote_ident(conname)='%s')"
+"         WHERE conname='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_rule_of_table \
-"SELECT pg_catalog.quote_ident(rulename) "\
+"SELECT rulename "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_rule \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE pg_catalog.quote_ident(rulename)='%s')"
+"         WHERE rulename='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_trigger_of_table \
-"SELECT pg_catalog.quote_ident(tgname) "\
+"SELECT tgname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"\
 "       and not tgisinternal"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_trigger \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE pg_catalog.quote_ident(tgname)='%s')"
+"         WHERE tgname='%s')"

 #define Query_for_list_of_ts_configurations \
-"SELECT pg_catalog.quote_ident(cfgname) FROM pg_catalog.pg_ts_config "\
-" WHERE substring(pg_catalog.quote_ident(cfgname),1,%d)='%s'"
+"SELECT cfgname FROM pg_catalog.pg_ts_config "\
+" WHERE substring(cfgname,1,%d)='%s'"

 #define Query_for_list_of_ts_dictionaries \
-"SELECT pg_catalog.quote_ident(dictname) FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(pg_catalog.quote_ident(dictname),1,%d)='%s'"
+"SELECT dictname FROM pg_catalog.pg_ts_dict "\
+" WHERE substring(dictname,1,%d)='%s'"

 #define Query_for_list_of_ts_parsers \
-"SELECT pg_catalog.quote_ident(prsname) FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(pg_catalog.quote_ident(prsname),1,%d)='%s'"
+"SELECT prsname FROM pg_catalog.pg_ts_parser "\
+" WHERE substring(prsname,1,%d)='%s'"

 #define Query_for_list_of_ts_templates \
-"SELECT pg_catalog.quote_ident(tmplname) FROM pg_catalog.pg_ts_template "\
-" WHERE substring(pg_catalog.quote_ident(tmplname),1,%d)='%s'"
+"SELECT tmplname FROM pg_catalog.pg_ts_template "\
+" WHERE substring(tmplname,1,%d)='%s'"

 #define Query_for_list_of_fdws \
-" SELECT pg_catalog.quote_ident(fdwname) "\
+" SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(pg_catalog.quote_ident(fdwname),1,%d)='%s'"
+"  WHERE substring(fdwname,1,%d)='%s'"

 #define Query_for_list_of_servers \
-" SELECT pg_catalog.quote_ident(srvname) "\
+" SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(pg_catalog.quote_ident(srvname),1,%d)='%s'"
+"  WHERE substring(srvname,1,%d)='%s'"

 #define Query_for_list_of_user_mappings \
-" SELECT pg_catalog.quote_ident(usename) "\
+" SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(pg_catalog.quote_ident(usename),1,%d)='%s'"
+"  WHERE substring(usename,1,%d)='%s'"

 #define Query_for_list_of_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s'"
+"  WHERE substring(amname,1,%d)='%s'"

 #define Query_for_list_of_index_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc "\
-" WHERE (%d = pg_catalog.length('%s'))"\
-"   AND (pg_catalog.quote_ident(proname)='%s'"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
+"   AND (proname='%s'"\
 "        OR '\"' || proname || '\"'='%s') "\
 "   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments_with_schema \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
 "   AND n.oid = p.pronamespace "\
-"   AND (pg_catalog.quote_ident(proname)='%s' "\
+"   AND (proname='%s' "\
 "        OR '\"' || proname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_extensions \
-" SELECT pg_catalog.quote_ident(extname) "\
+" SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(pg_catalog.quote_ident(extname),1,%d)='%s'"
+"  WHERE substring(extname,1,%d)='%s'"

 #define Query_for_list_of_available_extensions \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s' AND installed_version IS NULL"
+"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions \
-" SELECT pg_catalog.quote_ident(version) "\
+" SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring(version,1,%d)='%s'"\
+"    AND name='%s'"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions_with_TO \
-" SELECT 'TO ' || pg_catalog.quote_ident(version) "\
+" SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring('TO ' || version,1,%d)='%s'"\
+"    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_event_triggers \
-" SELECT pg_catalog.quote_ident(evtname) "\
+" SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(pg_catalog.quote_ident(evtname),1,%d)='%s'"
+"  WHERE substring(evtname,1,%d)='%s'"

 #define Query_for_list_of_tablesample_methods \
-" SELECT pg_catalog.quote_ident(proname) "\
+" SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(pg_catalog.quote_ident(proname),1,%d)='%s'"
+"        substring(proname,1,%d)='%s'"

 #define Query_for_list_of_policies \
-" SELECT pg_catalog.quote_ident(polname) "\
+" SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(pg_catalog.quote_ident(polname),1,%d)='%s'"
+"  WHERE substring(polname,1,%d)='%s'"

 #define Query_for_list_of_tables_for_policy \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE pg_catalog.quote_ident(polname)='%s')"
+"         WHERE polname='%s')"

 #define Query_for_enum \
 " SELECT name FROM ( "\
-"   SELECT pg_catalog.quote_ident(pg_catalog.unnest(enumvals)) AS name "\
+"   SELECT pg_catalog.unnest(enumvals) AS name "\
 "     FROM pg_catalog.pg_settings "\
 "    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
-"    UNION ALL " \
-"   SELECT 'DEFAULT' ) ss "\
+"    ) ss "\
 "  WHERE pg_catalog.substring(name,1,%%d)='%%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_partition_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
 " WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"\
 "       and c2.relispartition = 'true'"

 #define Query_for_list_of_cursors \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1043,18 +1110,18 @@ Query_for_index_of_table \

 static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(pubname) "
+        " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pg_catalog.quote_ident(pubname),1,%d)='%s'"
+        "  WHERE substring(pubname,1,%d)='%s'"
     },
     {0, NULL}
 };

 static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(s.subname) "
+        " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(pg_catalog.quote_ident(s.subname),1,%d)='%s' "
+        "  WHERE substring(s.subname,1,%d)='%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1069,9 +1136,11 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
 typedef struct
 {
     const char *name;
+    /* Provide at most one of these three types of query: */
     const char *query;            /* simple query, or NULL */
     const VersionedQuery *vquery;    /* versioned query, or NULL */
     const SchemaQuery *squery;    /* schema query, or NULL */
+    const char *const *keywords;    /* keywords to be offered as well */
     const bits32 flags;            /* visibility flags, see below */
 } pgsql_thing_t;

@@ -1080,8 +1149,14 @@ typedef struct
 #define THING_NO_ALTER        (1 << 2)    /* should not show up after ALTER */
 #define THING_NO_SHOW        (THING_NO_CREATE | THING_NO_DROP | THING_NO_ALTER)

+/* When we have DROP USER etc, also offer MAPPING FOR */
+static const char *const Keywords_for_user_thing[] = {
+    "MAPPING FOR",
+    NULL
+};
+
 static const pgsql_thing_t words_after_create[] = {
-    {"ACCESS METHOD", NULL, NULL, NULL, THING_NO_ALTER},
+    {"ACCESS METHOD", NULL, NULL, NULL, NULL, THING_NO_ALTER},
     {"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates},
     {"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
                                  * skip it */
@@ -1091,11 +1166,11 @@ static const pgsql_thing_t words_after_create[] = {
      * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
      * to be used only by pg_dump.
      */
-    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT pg_catalog.quote_ident(conname) FROM pg_catalog.pg_conversion WHERE
substring(pg_catalog.quote_ident(conname),1,%d)='%s'"},
+    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, NULL, THING_NO_SHOW},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
     {"DATABASE", Query_for_list_of_databases},
-    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
-    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, THING_NO_SHOW},
+    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, NULL, THING_NO_SHOW},
     {"DOMAIN", NULL, NULL, &Query_for_list_of_domains},
     {"EVENT TRIGGER", NULL, NULL, NULL},
     {"EXTENSION", Query_for_list_of_extensions},
@@ -1105,41 +1180,41 @@ static const pgsql_thing_t words_after_create[] = {
     {"GROUP", Query_for_list_of_roles},
     {"INDEX", NULL, NULL, &Query_for_list_of_indexes},
     {"LANGUAGE", Query_for_list_of_languages},
-    {"LARGE OBJECT", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews},
     {"OPERATOR", NULL, NULL, NULL}, /* Querying for this is probably not such
                                      * a good idea. */
-    {"OR REPLACE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
-    {"OWNED", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
-    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, THING_NO_SHOW},
+    {"OR REPLACE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
+    {"OWNED", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
+    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, NULL, THING_NO_SHOW},
     {"POLICY", NULL, NULL, NULL},
     {"PROCEDURE", NULL, NULL, Query_for_list_of_procedures},
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
-    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, THING_NO_CREATE},
-    {"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE
substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
+    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
     {"STATISTICS", NULL, NULL, &Query_for_list_of_statistics},
     {"SUBSCRIPTION", NULL, Query_for_list_of_subscriptions},
-    {"SYSTEM", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"SYSTEM", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"TABLE", NULL, NULL, &Query_for_list_of_tables},
     {"TABLESPACE", Query_for_list_of_tablespaces},
-    {"TEMP", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TEMP TABLE
-                                                                 * ... */
-    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, THING_NO_SHOW},
-    {"TEMPORARY", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
-                                                                         * TABLE ... */
+    {"TEMP", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMP TABLE
+                                                                         * ... */
+    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, NULL, THING_NO_SHOW},
+    {"TEMPORARY", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
+                                                                             * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
-    {"TRANSFORM", NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT pg_catalog.quote_ident(tgname) FROM pg_catalog.pg_trigger WHERE
substring(pg_catalog.quote_ident(tgname),1,%d)='%s'AND NOT tgisinternal"}, 
+    {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
-    {"UNIQUE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNIQUE
-                                                                     * INDEX ... */
-    {"UNLOGGED", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNLOGGED
-                                                                     * TABLE ... */
-    {"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"},
+    {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
+                                                                         * INDEX ... */
+    {"UNLOGGED", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNLOGGED
+                                                                             * TABLE ... */
+    {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
     {"USER MAPPING FOR", NULL, NULL, NULL},
     {"VIEW", NULL, NULL, &Query_for_list_of_views},
     {NULL}                        /* end of list */
@@ -1200,6 +1275,8 @@ static char *complete_from_schema_query(const char *text, int state);
 static char *complete_from_versioned_schema_query(const char *text, int state);
 static char *_complete_from_query(const char *simple_query,
                                   const SchemaQuery *schema_query,
+                                  const char *const *keywords,
+                                  bool verbatim,
                                   const char *text, int state);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
@@ -1212,6 +1289,12 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static void parse_identifier(const char *ident,
+                             char **schemaname, char **objectname,
+                             bool *schemaquoted, bool *objectquoted);
+static char *requote_identifier(const char *schemaname, const char *objectname,
+                                bool quote_schema, bool quote_object);
+static bool identifier_needs_quotes(const char *ident);
 static PGresult *exec_query(const char *query);

 static char **get_previous_words(int point, char **buffer, int *nwords);
@@ -1650,8 +1733,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE */
     else if (Matches("ALTER", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ALL IN TABLESPACE");

     /* ALTER something */
     else if (Matches("ALTER"))
@@ -1699,11 +1782,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
         COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     /* ALTER PUBLICATION <name> SET ( */
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny) && TailMatches("SET", "("))
         COMPLETE_WITH("publish", "publish_via_partition_root");
@@ -1782,14 +1865,14 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions_with_TO);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
         completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* ALTER FOREIGN */
@@ -1811,8 +1894,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER INDEX */
     else if (Matches("ALTER", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ALL IN TABLESPACE");
     /* ALTER INDEX <name> */
     else if (Matches("ALTER", "INDEX", MatchAny))
         COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET",
@@ -1821,7 +1904,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH"))
         COMPLETE_WITH("PARTITION");
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     /* ALTER INDEX <name> ALTER */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER"))
         COMPLETE_WITH("COLUMN");
@@ -1878,8 +1961,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER MATERIALIZED VIEW */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "ALL IN TABLESPACE");

     /* ALTER USER,ROLE <name> */
     else if (Matches("ALTER", "USER|ROLE", MatchAny) &&
@@ -1973,7 +2056,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "SYSTEM"))
         COMPLETE_WITH("SET", "RESET");
     else if (Matches("ALTER", "SYSTEM", "SET|RESET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_alter_system_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_alter_system_set_vars,
+                                 "all");
     else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
         COMPLETE_WITH("TO");
     /* ALTER VIEW <name> */
@@ -1982,9 +2066,9 @@ psql_completion(const char *text, int start, int end)
                       "SET SCHEMA");
     /* ALTER VIEW xxx RENAME */
     else if (Matches("ALTER", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER VIEW xxx ALTER [ COLUMN ] yyy */
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER", MatchAny) ||
              Matches("ALTER", "VIEW", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2003,9 +2087,9 @@ psql_completion(const char *text, int start, int end)
                       "RESET (", "SET");
     /* ALTER MATERIALIZED VIEW xxx RENAME */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER MATERIALIZED VIEW xxx RENAME yyy */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME", MatchAnyExcept("TO")))
         COMPLETE_WITH("TO");
@@ -2032,7 +2116,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RENAME TO", "TO", "USING (", "WITH CHECK (");
     /* ALTER POLICY <name> ON <table> TO <role> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* ALTER POLICY <name> ON <table> USING ( */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2073,7 +2158,7 @@ psql_completion(const char *text, int start, int end)
      * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
      */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
@@ -2100,7 +2185,7 @@ psql_completion(const char *text, int start, int end)
     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")))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     /* ALTER TABLE xxx ADD CONSTRAINT yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
         COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY");
@@ -2164,13 +2249,13 @@ psql_completion(const char *text, int start, int end)
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx NO */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO"))
         COMPLETE_WITH("FORCE ROW LEVEL SECURITY", "INHERIT");
     /* ALTER TABLE xxx NO INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO", "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx DISABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE"))
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
@@ -2187,13 +2272,13 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE xxx ALTER */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT");

     /* ALTER TABLE xxx RENAME */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT", "TO");
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

     /* ALTER TABLE xxx RENAME yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME", MatchAnyExcept("CONSTRAINT|TO")))
@@ -2208,7 +2293,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("COLUMN", "CONSTRAINT");
     /* If we have ALTER TABLE <sth> DROP COLUMN, provide list of columns */
     else if (Matches("ALTER", "TABLE", MatchAny, "DROP", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
@@ -2298,7 +2383,7 @@ psql_completion(const char *text, int start, int end)
      * tables.
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("ATTACH", "PARTITION", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2360,7 +2445,7 @@ psql_completion(const char *text, int start, int end)
      * of attributes
      */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER|DROP|RENAME", "ATTRIBUTE"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TYPE ALTER ATTRIBUTE <foo> */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER", "ATTRIBUTE", MatchAny))
         COMPLETE_WITH("TYPE");
@@ -2385,8 +2470,8 @@ psql_completion(const char *text, int start, int end)
  * ANALYZE [ VERBOSE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables,
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_analyzables,
+                                        "VERBOSE");
     else if (HeadMatches("ANALYZE", "(*") &&
              !HeadMatches("ANALYZE", "(*)"))
     {
@@ -2402,9 +2487,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("ANALYZE") && TailMatches("("))
         /* "ANALYZE (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables);

 /* BEGIN */
     else if (Matches("BEGIN"))
@@ -2425,19 +2510,20 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("CHAIN");
 /* CALL */
     else if (Matches("CALL"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
     else if (Matches("CALL", MatchAny))
         COMPLETE_WITH("(");
 /* CLOSE */
     else if (Matches("CLOSE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL");
 /* CLUSTER */
     else if (Matches("CLUSTER"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, "UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables,
+                                        "VERBOSE");
     else if (Matches("CLUSTER", "VERBOSE") ||
              Matches("CLUSTER", "(*)"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables);
     /* If we have CLUSTER <sth>, then add "USING" */
     else if (Matches("CLUSTER", MatchAnyExcept("VERBOSE|ON|(|(*)")))
         COMPLETE_WITH("USING");
@@ -2487,19 +2573,19 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_constraint
-                            " UNION SELECT 'DOMAIN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                 "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (Matches("COMMENT", "ON", "EVENT", "TRIGGER"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);
     else if (Matches("COMMENT", "ON", "FOREIGN"))
         COMPLETE_WITH("DATA WRAPPER", "TABLE");
     else if (Matches("COMMENT", "ON", "FOREIGN", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (Matches("COMMENT", "ON", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("COMMENT", "ON", "POLICY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_policies);
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny))
@@ -2529,7 +2615,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH", "TEMPLATE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_templates);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -2557,8 +2643,7 @@ psql_completion(const char *text, int start, int end)
      * backslash command).
      */
     else if (Matches("COPY|\\copy"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION ALL SELECT '('");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables, "(");
     /* Complete COPY ( with legal query commands */
     else if (Matches("COPY|\\copy", "("))
         COMPLETE_WITH("SELECT", "TABLE", "VALUES", "INSERT INTO", "UPDATE", "DELETE FROM", "WITH");
@@ -2616,7 +2701,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "COLLATION", MatchAny))
         COMPLETE_WITH("(", "FROM");
     else if (Matches("CREATE", "COLLATION", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);
     else if (HeadMatches("CREATE", "COLLATION", MatchAny, "(*"))
     {
         if (TailMatches("(|*,"))
@@ -2642,12 +2727,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "DOMAIN", MatchAny))
         COMPLETE_WITH("AS");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS", MatchAny))
         COMPLETE_WITH("COLLATE", "DEFAULT", "CONSTRAINT",
                       "NOT NULL", "NULL", "CHECK (");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "COLLATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);

     /* CREATE EXTENSION */
     /* Complete with available extensions rather than installed ones. */
@@ -2660,7 +2745,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* CREATE FOREIGN */
@@ -2685,9 +2770,8 @@ psql_completion(const char *text, int start, int end)
      * existing indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'"
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON", "CONCURRENTLY");

     /*
      * Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of relations
@@ -2695,15 +2779,15 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("INDEX|CONCURRENTLY", MatchAny, "ON") ||
              TailMatches("INDEX|CONCURRENTLY", "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);

     /*
      * Complete CREATE|UNIQUE INDEX CONCURRENTLY with "ON" and existing
      * indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON");
     /* Complete CREATE|UNIQUE INDEX [CONCURRENTLY] <sth> with "ON" */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", MatchAny) ||
              TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY", MatchAny))
@@ -2718,10 +2802,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("(", "USING");
     else if (TailMatches("INDEX", MatchAny, "ON", MatchAny, "(") ||
              TailMatches("INDEX|CONCURRENTLY", "ON", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* same if you put in USING */
     else if (TailMatches("ON", MatchAny, "USING", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev4_wd, "");
+        COMPLETE_WITH_ATTR(prev4_wd);
     /* Complete USING with an index method */
     else if (TailMatches("INDEX", MatchAny, MatchAny, "ON", MatchAny, "USING") ||
              TailMatches("INDEX", MatchAny, "ON", MatchAny, "USING") ||
@@ -2742,7 +2826,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     /* Complete "CREATE POLICY <name> ON <table>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Complete "CREATE POLICY <name> ON <table> AS|FOR|TO|USING|WITH CHECK" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("AS", "FOR", "TO", "USING (", "WITH CHECK (");
@@ -2770,7 +2854,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO", "USING (", "WITH CHECK (");
     /* Complete "CREATE POLICY <name> ON <table> TO <role>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "CREATE POLICY <name> ON <table> USING (" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2808,7 +2893,8 @@ psql_completion(const char *text, int start, int end)
      * <role>"
      */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "AS", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);

     /*
      * Complete "CREATE POLICY <name> ON <table> AS PERMISSIVE|RESTRICTIVE
@@ -2831,18 +2917,18 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete "CREATE PUBLICATION <name> FOR ALL TABLES IN SCHEMA <schema>,
      * ..."
      */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA' ");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) &&
(!ends_with(prev_wd,','))) 
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> [...] WITH" */
@@ -2871,7 +2957,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO");
     /* Complete "AS ON <sth> TO" with a table name */
     else if (TailMatches("AS", "ON", "SELECT|UPDATE|INSERT|DELETE", "TO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE SEQUENCE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     else if (TailMatches("CREATE", "SEQUENCE", MatchAny) ||
@@ -2898,7 +2984,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (HeadMatches("CREATE", "STATISTICS", MatchAny) &&
              TailMatches("FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
@@ -2912,7 +2998,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RANGE (", "LIST (", "HASH (");
     /* If we have xxx PARTITION OF, provide a list of partitioned tables */
     else if (TailMatches("PARTITION", "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("PARTITION", "OF", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2923,7 +3009,7 @@ psql_completion(const char *text, int start, int end)
     /* Complete CREATE TABLE <name> OF with list of composite types */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "OF") ||
              TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
     /* Complete CREATE TABLE name (...) with supported options */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)") ||
              TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)"))
@@ -2962,7 +3048,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "TRANSFORM", "FOR") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny) ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
@@ -3030,7 +3116,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete CREATE [ OR REPLACE ] TRIGGER ... INSTEAD OF event ON with a
@@ -3038,7 +3124,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("ON", MatchAny))
@@ -3143,7 +3229,7 @@ psql_completion(const char *text, int start, int end)
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* CREATE ROLE,USER,GROUP <name> */
     else if (Matches("CREATE", "ROLE|GROUP|USER", MatchAny) &&
@@ -3179,7 +3265,7 @@ psql_completion(const char *text, int start, int end)
     else if (HeadMatches("CREATE", "TYPE", MatchAny, "AS", "("))
     {
         if (TailMatches("(|*,", MatchAny))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("(|*,", MatchAny, MatchAnyExcept("*)")))
             COMPLETE_WITH("COLLATE", ",", ")");
     }
@@ -3263,12 +3349,12 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("CREATE", "EVENT", "TRIGGER") &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* DEALLOCATE */
     else if (Matches("DEALLOCATE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_prepared_statements,
+                                 "ALL");

 /* DECLARE */

@@ -3316,7 +3402,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FROM");
     /* Complete DELETE FROM with a list of tables */
     else if (TailMatches("DELETE", "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete DELETE FROM <table> */
     else if (TailMatches("DELETE", "FROM", MatchAny))
         COMPLETE_WITH("USING", "WHERE");
@@ -3358,10 +3444,10 @@ psql_completion(const char *text, int start, int end)

     /* DROP INDEX */
     else if (Matches("DROP", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("DROP", "INDEX", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY", MatchAny))
@@ -3371,7 +3457,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("DROP", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("DROP", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");

@@ -3440,7 +3526,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "TRANSFORM"))
         COMPLETE_WITH("FOR");
     else if (Matches("DROP", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -3494,28 +3580,28 @@ psql_completion(const char *text, int start, int end)
      * NEXT, PRIOR, FIRST, LAST, FROM, IN, and a list of cursors
      */
     else if (Matches("FETCH|MOVE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ABSOLUTE'"
-                            " UNION SELECT 'BACKWARD'"
-                            " UNION SELECT 'FORWARD'"
-                            " UNION SELECT 'RELATIVE'"
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'NEXT'"
-                            " UNION SELECT 'PRIOR'"
-                            " UNION SELECT 'FIRST'"
-                            " UNION SELECT 'LAST'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ABSOLUTE",
+                                 "BACKWARD",
+                                 "FORWARD",
+                                 "RELATIVE",
+                                 "ALL",
+                                 "NEXT",
+                                 "PRIOR",
+                                 "FIRST",
+                                 "LAST",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH BACKWARD or FORWARD with one of ALL, FROM, IN, and a
      * list of cursors
      */
     else if (Matches("FETCH|MOVE", "BACKWARD|FORWARD"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH <direction> with "FROM" or "IN". These are equivalent,
@@ -3525,9 +3611,9 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("FETCH|MOVE", "ABSOLUTE|BACKWARD|FORWARD|RELATIVE",
                      MatchAnyExcept("FROM|IN")) ||
              Matches("FETCH|MOVE", "ALL|NEXT|PRIOR|FIRST|LAST"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "FROM",
+                                 "IN");
     /* Complete FETCH <direction> "FROM" or "IN" with a list of cursors */
     else if (HeadMatches("FETCH|MOVE") &&
              TailMatches("FROM|IN"))
@@ -3546,7 +3632,7 @@ psql_completion(const char *text, int start, int end)
 /* FOREIGN TABLE */
     else if (TailMatches("FOREIGN", "TABLE") &&
              !TailMatches("CREATE", MatchAny, MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);

 /* FOREIGN SERVER */
     else if (TailMatches("FOREIGN", "SERVER"))
@@ -3568,20 +3654,20 @@ psql_completion(const char *text, int start, int end)
                           "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
                           "EXECUTE", "USAGE", "ALL");
         else
-            COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                                " UNION SELECT 'SELECT'"
-                                " UNION SELECT 'INSERT'"
-                                " UNION SELECT 'UPDATE'"
-                                " UNION SELECT 'DELETE'"
-                                " UNION SELECT 'TRUNCATE'"
-                                " UNION SELECT 'REFERENCES'"
-                                " UNION SELECT 'TRIGGER'"
-                                " UNION SELECT 'CREATE'"
-                                " UNION SELECT 'CONNECT'"
-                                " UNION SELECT 'TEMPORARY'"
-                                " UNION SELECT 'EXECUTE'"
-                                " UNION SELECT 'USAGE'"
-                                " UNION SELECT 'ALL'");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                     "SELECT",
+                                     "INSERT",
+                                     "UPDATE",
+                                     "DELETE",
+                                     "TRUNCATE",
+                                     "REFERENCES",
+                                     "TRIGGER",
+                                     "CREATE",
+                                     "CONNECT",
+                                     "TEMPORARY",
+                                     "EXECUTE",
+                                     "USAGE",
+                                     "ALL");
     }

     /*
@@ -3601,9 +3687,6 @@ psql_completion(const char *text, int start, int end)
     /*
      * Complete GRANT/REVOKE <sth> ON with a list of appropriate relations.
      *
-     * Keywords like DATABASE, FUNCTION, LANGUAGE and SCHEMA added to query
-     * result via UNION; seems to work intuitively.
-     *
      * Note: GRANT/REVOKE can get quite complex; tab-completion as implemented
      * here will only work if the privilege list contains exactly one
      * privilege.
@@ -3617,26 +3700,26 @@ psql_completion(const char *text, int start, int end)
         if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
             COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
         else
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables,
-                                       " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
-                                       " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
-                                       " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
-                                       " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
-                                       " UNION SELECT 'ALL TABLES IN SCHEMA'"
-                                       " UNION SELECT 'DATABASE'"
-                                       " UNION SELECT 'DOMAIN'"
-                                       " UNION SELECT 'FOREIGN DATA WRAPPER'"
-                                       " UNION SELECT 'FOREIGN SERVER'"
-                                       " UNION SELECT 'FUNCTION'"
-                                       " UNION SELECT 'LANGUAGE'"
-                                       " UNION SELECT 'LARGE OBJECT'"
-                                       " UNION SELECT 'PROCEDURE'"
-                                       " UNION SELECT 'ROUTINE'"
-                                       " UNION SELECT 'SCHEMA'"
-                                       " UNION SELECT 'SEQUENCE'"
-                                       " UNION SELECT 'TABLE'"
-                                       " UNION SELECT 'TABLESPACE'"
-                                       " UNION SELECT 'TYPE'");
+            COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
+                                            "ALL FUNCTIONS IN SCHEMA",
+                                            "ALL PROCEDURES IN SCHEMA",
+                                            "ALL ROUTINES IN SCHEMA",
+                                            "ALL SEQUENCES IN SCHEMA",
+                                            "ALL TABLES IN SCHEMA",
+                                            "DATABASE",
+                                            "DOMAIN",
+                                            "FOREIGN DATA WRAPPER",
+                                            "FOREIGN SERVER",
+                                            "FUNCTION",
+                                            "LANGUAGE",
+                                            "LARGE OBJECT",
+                                            "PROCEDURE",
+                                            "ROUTINE",
+                                            "SCHEMA",
+                                            "SEQUENCE",
+                                            "TABLE",
+                                            "TABLESPACE",
+                                            "TYPE");
     }
     else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
         COMPLETE_WITH("FUNCTIONS IN SCHEMA",
@@ -3658,25 +3741,25 @@ psql_completion(const char *text, int start, int end)
         if (TailMatches("DATABASE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_databases);
         else if (TailMatches("DOMAIN"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
         else if (TailMatches("FUNCTION"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
         else if (TailMatches("LANGUAGE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_languages);
         else if (TailMatches("PROCEDURE"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
         else if (TailMatches("ROUTINE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
         else if (TailMatches("SCHEMA"))
             COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
         else if (TailMatches("SEQUENCE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
         else if (TailMatches("TABLE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
         else if (TailMatches("TABLESPACE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
         else if (TailMatches("TYPE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
             COMPLETE_WITH("TO");
         else
@@ -3689,10 +3772,12 @@ psql_completion(const char *text, int start, int end)
      */
     else if ((HeadMatches("GRANT") && TailMatches("TO")) ||
              (HeadMatches("REVOKE") && TailMatches("FROM")))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "ALTER DEFAULT PRIVILEGES ... GRANT/REVOKE ... TO/FROM */
     else if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES") && TailMatches("TO|FROM"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "GRANT/REVOKE ... ON * *" with TO/FROM */
     else if (HeadMatches("GRANT") && TailMatches("ON", MatchAny, MatchAny))
         COMPLETE_WITH("TO");
@@ -3753,10 +3838,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("INTO");
     /* Complete INSERT INTO with table names */
     else if (TailMatches("INSERT", "INTO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete "INSERT INTO <table> (" with attribute names */
     else if (TailMatches("INSERT", "INTO", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

     /*
      * Complete INSERT INTO <table> with "(" or "VALUES" or "SELECT" or
@@ -3788,14 +3873,13 @@ psql_completion(const char *text, int start, int end)
 /* LOCK */
     /* Complete LOCK [TABLE] [ONLY] with a list of tables */
     else if (Matches("LOCK"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "TABLE", "ONLY");
     else if (Matches("LOCK", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ONLY");
     else if (Matches("LOCK", "TABLE", "ONLY") || Matches("LOCK", "ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* For the following, handle the case of a single table only for now */

     /* Complete LOCK [TABLE] [ONLY] <table> with IN or NOWAIT */
@@ -3831,7 +3915,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s'"); 
+        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -3845,7 +3929,7 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatches("FROM", MatchAny, "ORDER"))
         COMPLETE_WITH("BY");
     else if (TailMatches("FROM", MatchAny, "ORDER", "BY"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

 /* PREPARE xx AS */
     else if (Matches("PREPARE", MatchAny, "AS"))
@@ -3874,10 +3958,10 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("REFRESH", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "CONCURRENTLY");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("WITH");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY", MatchAny))
@@ -3897,26 +3981,26 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TABLE", "INDEX", "SYSTEM", "SCHEMA", "DATABASE");
     else if (Matches("REINDEX", "TABLE") ||
              Matches("REINDEX", "(*)", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexables,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "INDEX") ||
              Matches("REINDEX", "(*)", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "SCHEMA") ||
              Matches("REINDEX", "(*)", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "SYSTEM|DATABASE") ||
              Matches("REINDEX", "(*)", "SYSTEM|DATABASE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_databases
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_databases,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "TABLE", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "TABLE", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);
     else if (Matches("REINDEX", "INDEX", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("REINDEX", "SCHEMA", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "SCHEMA", "CONCURRENTLY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
@@ -3960,9 +4044,17 @@ psql_completion(const char *text, int start, int end)
 /* SET, RESET, SHOW */
     /* Complete with a variable name */
     else if (TailMatches("SET|RESET") && !TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_set_vars,
+                                 "constraints",
+                                 "transaction",
+                                 "session",
+                                 "role",
+                                 "tablespace",
+                                 "all");
     else if (Matches("SHOW"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_show_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_show_vars,
+                                 "session authorization",
+                                 "all");
     /* Complete "SET TRANSACTION" */
     else if (Matches("SET", "TRANSACTION"))
         COMPLETE_WITH("SNAPSHOT", "ISOLATION LEVEL", "READ", "DEFERRABLE", "NOT DEFERRABLE");
@@ -3997,7 +4089,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ONLY", "WRITE");
     /* SET CONSTRAINTS */
     else if (Matches("SET", "CONSTRAINTS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema, "UNION SELECT 'ALL'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_constraints_with_schema,
+                                        "ALL");
     /* Complete SET CONSTRAINTS <foo> with DEFERRED|IMMEDIATE */
     else if (Matches("SET", "CONSTRAINTS", MatchAny))
         COMPLETE_WITH("DEFERRED", "IMMEDIATE");
@@ -4009,7 +4102,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("AUTHORIZATION", "CHARACTERISTICS AS TRANSACTION");
     /* Complete SET SESSION AUTHORIZATION with username */
     else if (Matches("SET", "SESSION", "AUTHORIZATION"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles " UNION SELECT 'DEFAULT'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "DEFAULT");
     /* Complete RESET SESSION with AUTHORIZATION */
     else if (Matches("RESET", "SESSION"))
         COMPLETE_WITH("AUTHORIZATION");
@@ -4039,10 +4133,10 @@ psql_completion(const char *text, int start, int end)
                           "US", "European", "NonEuropean",
                           "DEFAULT");
         else if (TailMatches("search_path", "TO|="))
-            COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                                " AND nspname not like 'pg\\_toast%%' "
-                                " AND nspname not like 'pg\\_temp%%' "
-                                " UNION SELECT 'DEFAULT' ");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                     " AND nspname not like 'pg\\_toast%%' "
+                                     " AND nspname not like 'pg\\_temp%%' ",
+                                     "DEFAULT");
         else
         {
             /* generic, type based, GUC support */
@@ -4061,7 +4155,7 @@ psql_completion(const char *text, int start, int end)

                     snprintf(querybuf, sizeof(querybuf),
                              Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY(querybuf);
+                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4080,7 +4174,7 @@ psql_completion(const char *text, int start, int end)

 /* TABLE, but not TABLE embedded in other commands */
     else if (Matches("TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* TABLESAMPLE */
     else if (TailMatches("TABLESAMPLE"))
@@ -4090,14 +4184,13 @@ psql_completion(const char *text, int start, int end)

 /* TRUNCATE */
     else if (Matches("TRUNCATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "TABLE", "ONLY");
     else if (Matches("TRUNCATE", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "ONLY");
     else if (HeadMatches("TRUNCATE") && TailMatches("ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables);
     else if (Matches("TRUNCATE", MatchAny) ||
              Matches("TRUNCATE", "TABLE|ONLY", MatchAny) ||
              Matches("TRUNCATE", "TABLE", "ONLY", MatchAny))
@@ -4107,18 +4200,20 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s' UNION SELECT '*'"); 
+        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
+                                 " WHERE substring(channel,1,%d)='%s'",
+                                 "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
     else if (TailMatches("UPDATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete UPDATE <table> with "SET" */
     else if (TailMatches("UPDATE", MatchAny))
         COMPLETE_WITH("SET");
     /* Complete UPDATE <table> SET with list of attributes */
     else if (TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* UPDATE <table> SET <attr> = */
     else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
         COMPLETE_WITH("=");
@@ -4127,11 +4222,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER|CREATE|DROP", "USER", "MAPPING"))
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "USER", "MAPPING", "FOR"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                            " UNION SELECT 'CURRENT_ROLE'"
-                            " UNION SELECT 'CURRENT_USER'"
-                            " UNION SELECT 'PUBLIC'"
-                            " UNION SELECT 'USER'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "CURRENT_ROLE",
+                                 "CURRENT_USER",
+                                 "PUBLIC",
+                                 "USER");
     else if (Matches("ALTER|DROP", "USER", "MAPPING", "FOR"))
         COMPLETE_WITH_QUERY(Query_for_list_of_user_mappings);
     else if (Matches("CREATE|ALTER|DROP", "USER", "MAPPING", "FOR", MatchAny))
@@ -4144,26 +4239,26 @@ psql_completion(const char *text, int start, int end)
  * VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FULL'"
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FULL",
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FULL"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FREEZE") ||
              Matches("VACUUM", "FULL", "FREEZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'VERBOSE'"
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "VERBOSE",
+                                        "ANALYZE");
     else if (Matches("VACUUM", "VERBOSE") ||
              Matches("VACUUM", "FULL|FREEZE", "VERBOSE") ||
              Matches("VACUUM", "FULL", "FREEZE", "VERBOSE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "ANALYZE");
     else if (HeadMatches("VACUUM", "(*") &&
              !HeadMatches("VACUUM", "(*)"))
     {
@@ -4184,9 +4279,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("VACUUM") && TailMatches("("))
         /* "VACUUM (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables);

 /* WITH [RECURSIVE] */

@@ -4200,16 +4295,16 @@ psql_completion(const char *text, int start, int end)
 /* WHERE */
     /* Simple case of the word before the where being the table name */
     else if (TailMatches(MatchAny, "WHERE"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

 /* ... FROM ... */
 /* TODO: also include SRF ? */
     else if (TailMatches("FROM") && !Matches("COPY|\\copy", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* ... JOIN ... */
     else if (TailMatches("JOIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* Backslash commands */
 /* TODO:  \dc \dd \dl */
@@ -4226,19 +4321,19 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     }
     else if (TailMatchesCS("\\da*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates);
     else if (TailMatchesCS("\\dAc*", MatchAny) ||
              TailMatchesCS("\\dAf*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dAo*", MatchAny) ||
              TailMatchesCS("\\dAp*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families);
     else if (TailMatchesCS("\\dA*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
     else if (TailMatchesCS("\\db*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
     else if (TailMatchesCS("\\dD*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (TailMatchesCS("\\des*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_servers);
     else if (TailMatchesCS("\\deu*"))
@@ -4246,9 +4341,9 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatchesCS("\\dew*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_fdws);
     else if (TailMatchesCS("\\df*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
     else if (HeadMatchesCS("\\df*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);

     else if (TailMatchesCS("\\dFd*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_dictionaries);
@@ -4261,51 +4356,51 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_configurations);

     else if (TailMatchesCS("\\di*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (TailMatchesCS("\\dL*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     else if (TailMatchesCS("\\dn*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
     /* no support for completing operators, but we can complete types: */
     else if (HeadMatchesCS("\\do*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dp") || TailMatchesCS("\\z"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
     else if (TailMatchesCS("\\dPi*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes);
     else if (TailMatchesCS("\\dPt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     else if (TailMatchesCS("\\dP*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations);
     else if (TailMatchesCS("\\ds*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
     else if (TailMatchesCS("\\dt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     else if (TailMatchesCS("\\dT*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\du*") || TailMatchesCS("\\dg*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     else if (TailMatchesCS("\\dv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\dx*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
     else if (TailMatchesCS("\\dX*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics);
     else if (TailMatchesCS("\\dm*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (TailMatchesCS("\\dE*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (TailMatchesCS("\\dy*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);

     /* must be at end of \d alternatives: */
     else if (TailMatchesCS("\\d*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations);

     else if (TailMatchesCS("\\ef"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\ev"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
         COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
@@ -4407,9 +4502,9 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_CS("default", "verbose", "terse", "sqlstate");
     }
     else if (TailMatchesCS("\\sf*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\sv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|"
                            "\\ir|\\include_relative|\\o|\\out|"
                            "\\s|\\w|\\write|\\lo_import"))
@@ -4426,19 +4521,21 @@ psql_completion(const char *text, int start, int end)
      */
     else
     {
-        int            i;
+        const pgsql_thing_t *wac;

-        for (i = 0; words_after_create[i].name; i++)
+        for (wac = words_after_create; wac->name != NULL; wac++)
         {
-            if (pg_strcasecmp(prev_wd, words_after_create[i].name) == 0)
+            if (pg_strcasecmp(prev_wd, wac->name) == 0)
             {
-                if (words_after_create[i].query)
-                    COMPLETE_WITH_QUERY(words_after_create[i].query);
-                else if (words_after_create[i].vquery)
-                    COMPLETE_WITH_VERSIONED_QUERY(words_after_create[i].vquery);
-                else if (words_after_create[i].squery)
-                    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(words_after_create[i].squery,
-                                                         NULL);
+                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;
             }
         }
@@ -4551,7 +4648,8 @@ static char *
 complete_from_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, NULL, text, state);
+    return _complete_from_query(completion_charp, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
@@ -4566,22 +4664,22 @@ complete_from_versioned_query(const char *text, int state)
     if (vquery->query == NULL)
         return NULL;

-    return _complete_from_query(vquery->query, NULL, text, state);
+    return _complete_from_query(vquery->query, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
 complete_from_schema_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, completion_squery,
-                                text, state);
+    return _complete_from_query(NULL, completion_squery, completion_charpp,
+                                false, text, state);
 }

 static char *
 complete_from_versioned_schema_query(const char *text, int state)
 {
     const SchemaQuery *squery = completion_squery;
-    const VersionedQuery *vquery = completion_vquery;

     /* Find appropriate array element */
     while (pset.sversion < squery->min_server_version)
@@ -4590,17 +4688,8 @@ complete_from_versioned_schema_query(const char *text, int state)
     if (squery->catname == NULL)
         return NULL;

-    /* Likewise for the add-on text, if any */
-    if (vquery)
-    {
-        while (pset.sversion < vquery->min_server_version)
-            vquery++;
-        if (vquery->query == NULL)
-            return NULL;
-    }
-
-    return _complete_from_query(vquery ? vquery->query : NULL,
-                                squery, text, state);
+    return _complete_from_query(NULL, squery, completion_charpp,
+                                false, text, state);
 }


@@ -4611,35 +4700,51 @@ complete_from_versioned_schema_query(const char *text, int state)
  *
  * The query can be one of two kinds:
  *
- * 1. A simple query which must contain a %d and a %s, which will be replaced
- * by the string length of the text and the text itself. The query may also
- * have up to four more %s in it; the first two such will be replaced by the
- * value of completion_info_charp, the next two by the value of
- * completion_info_charp2.
+ * 1. A simple query, which must contain a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
+ * The query may also have up to four more %s in it; the first two such will
+ * be replaced by the value of completion_info_charp, the next two by the
+ * value of completion_info_charp2.  (These strings will be escaped to
+ * become SQL literals, so what is actually in the query should be '%s'.)
+ * Simple queries should return a single column of matches.  If "verbatim"
+ * is true, the matches are returned as-is; otherwise, they are taken to
+ * be SQL identifiers and quoted if necessary.
  *
  * 2. A schema query used for completion of both schema and relation names.
- * These are more complex and must contain in the following order:
- * %d %s %d %s %d %s %s %d %s
- * where %d is the string length of the text and %s the text itself.
+ * This is represented by a SchemaQuery object; see that typedef for details.
+ *
+ * See top of file for examples of both kinds of query.
  *
- * If both simple_query and schema_query are non-NULL, then we construct
- * a schema query and append the (uninterpreted) string simple_query to it.
+ * In addition to the query itself, we accept a null-terminated array of
+ * literal keywords, which will be returned if they match the input-so-far
+ * (case insensitively).  (These are in addition to keywords specified
+ * within the schema_query, if any.)
  *
- * It is assumed that strings should be escaped to become SQL literals
- * (that is, what is in the query is actually ... '%s' ...)
+ * If "verbatim" is true, then we use the given text as-is to match the
+ * query results; otherwise we parse it as a possibly-qualified identifier,
+ * and reconstruct suitable quoting afterward.
  *
- * See top of file for examples of both kinds of query.
+ * "text" and "state" are supplied by Readline.  "text" is the word we are
+ * trying to complete.  "state" is zero on first call, nonzero later.
  *
- * "text" and "state" are supplied by readline.
+ * readline will call this repeatedly with the same text and varying
+ * state.  On each call, we are supposed to return a malloc'd string
+ * that is a candidate completion.  Return NULL when done.
  */
 static char *
 _complete_from_query(const char *simple_query,
                      const SchemaQuery *schema_query,
+                     const char *const *keywords,
+                     bool verbatim,
                      const char *text, int state)
 {
-    static int    list_index,
-                byte_length;
+    static int    list_index;
     static PGresult *result = NULL;
+    static bool schemaquoted;
+    static bool objectquoted;

     /*
      * If this is the first time for this completion, we fetch a list of our
@@ -4648,31 +4753,56 @@ _complete_from_query(const char *simple_query,
     if (state == 0)
     {
         PQExpBufferData query_buffer;
-        char       *e_text;
+        char       *schemaname;
+        char       *objectname;
+        int            schema_length = 0;
+        int            object_length = 0;
+        char       *e_schemaname;
+        char       *e_objectname;
         char       *e_info_charp;
         char       *e_info_charp2;
-        const char *pstr = text;
-        int            char_length = 0;

+        /* Reset static state, ensuring no memory leaks */
         list_index = 0;
-        byte_length = strlen(text);
+        PQclear(result);
+        result = NULL;
+
+        /* Parse text, splitting into schema and object name if needed */
+        if (verbatim)
+        {
+            objectname = pg_strdup(text);
+            schemaname = NULL;
+        }
+        else
+        {
+            parse_identifier(text,
+                             &schemaname, &objectname,
+                             &schemaquoted, &objectquoted);
+        }

         /*
-         * Count length as number of characters (not bytes), for passing to
+         * Count lengths as number of characters (not bytes), for passing to
          * substring
          */
-        while (*pstr)
+        if (schemaname)
         {
-            char_length++;
-            pstr += PQmblenBounded(pstr, pset.encoding);
+            for (const char *p = schemaname;
+                 *p;
+                 p += PQmblenBounded(p, pset.encoding))
+                schema_length++;
         }
-
-        /* Free any prior result */
-        PQclear(result);
-        result = NULL;
+        for (const char *p = objectname;
+             *p;
+             p += PQmblenBounded(p, pset.encoding))
+            object_length++;

         /* Set up suitably-escaped copies of textual inputs */
-        e_text = escape_string(text);
+        if (schemaname)
+            e_schemaname = escape_string(schemaname);
+        else
+            e_schemaname = NULL;
+
+        e_objectname = escape_string(objectname);

         if (completion_info_charp)
             e_info_charp = escape_string(completion_info_charp);
@@ -4688,14 +4818,17 @@ _complete_from_query(const char *simple_query,

         if (schema_query)
         {
-            /* schema_query gives us the pieces to assemble */
-            const char *qualresult = schema_query->qualresult;
-
-            if (qualresult == NULL)
-                qualresult = schema_query->result;
+            Assert(simple_query == NULL);

+            /*
+             * We issue different queries depending on whether the input is
+             * already qualified or not.  schema_query gives us the pieces to
+             * assemble.
+             */
+            if (schemaname == NULL)
+            {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s FROM %s WHERE ",
+            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
                               schema_query->result,
                               schema_query->catname);
             if (schema_query->selcondition)
@@ -4703,7 +4836,7 @@ _complete_from_query(const char *simple_query,
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
-                              char_length, e_text);
+                              object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);

@@ -4715,72 +4848,59 @@ _complete_from_query(const char *simple_query,
              */
             if (strcmp(schema_query->catname,
                        "pg_catalog.pg_class c") == 0 &&
-                strncmp(text, "pg_", 3) != 0)
+                strncmp(objectname, "pg_", 3) != 0)
             {
                 appendPQExpBufferStr(&query_buffer,
                                      " AND c.relnamespace <> (SELECT oid FROM"
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

-            /*
-             * Add in matching schema names, but only if there is more than
-             * one potential match among schema names.
-             */
+            /* Add in schema names matching the input-so-far */
             appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' "
+                              "SELECT NULL::pg_catalog.text, n.nspname "
                               "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d)='%s'",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) > 1",
-                              char_length, e_text);
+                              "WHERE substring(n.nspname,1,%d)='%s'",
+                              object_length, e_objectname);

             /*
-             * Add in matching qualified names, but only if there is exactly
-             * one schema matching the input-so-far.
+             * Likewise, suppress system schemas unless the input-so-far
+             * begins with "pg_".
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' || %s "
+            if (strncmp(objectname, "pg_", 3) != 0)
+                appendPQExpBufferStr(&query_buffer,
+                                     " AND n.nspname NOT LIKE 'pg\\_%'");
+
+            /*
+             * If the input-so-far had quotes, force any added schema names
+             * to be quoted, too.
+             */
+            schemaquoted = objectquoted;
+            }
+            else
+            {
+            /* Input is qualified, so produce only qualified names */
+            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
                               "FROM %s, pg_catalog.pg_namespace n "
                               "WHERE %s = n.oid AND ",
-                              qualresult,
+                              schema_query->result,
                               schema_query->catname,
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(pg_catalog.quote_ident(n.nspname) || '.' || %s,1,%d)='%s'",
-                              qualresult,
-                              char_length, e_text);
-
-            /*
-             * This condition exploits the single-matching-schema rule to
-             * speed up the query
-             */
-            appendPQExpBuffer(&query_buffer,
-                              " AND substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(n.nspname))+1)",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1",
-                              char_length, e_text);
-
-            /* If an addon query was provided, use it */
-            if (simple_query)
-                appendPQExpBuffer(&query_buffer, "\n%s", simple_query);
+            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                              schema_query->result,
+                              object_length, e_objectname);
+            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                              e_schemaname);
+            }
         }
         else
         {
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              char_length, e_text,
+                              object_length, e_objectname,
                               e_info_charp, e_info_charp,
                               e_info_charp2, e_info_charp2);
         }
@@ -4789,28 +4909,118 @@ _complete_from_query(const char *simple_query,
         appendPQExpBuffer(&query_buffer, "\nLIMIT %d",
                           completion_max_records);

+        /* Finally, we can issue the query */
         result = exec_query(query_buffer.data);

+        /*
+         * If we have a schema name, and any of the resulting object names
+         * require quoting, we have to force them all to be quoted.  This
+         * copes with a Readline heuristic that causes it to actually remove
+         * user-typed characters that are beyond the common prefix of what we
+         * return, that is if the user types foo.bar<TAB> and we return both
+         * foo.barbaz and foo."barXYZ", Readline actually discards "bar",
+         * which is not helpful at all.  That heuristic doesn't seem to be
+         * applied if the common prefix is empty, so we don't need to do this
+         * if dealing with unqualified names.
+         */
+        if (result && PQresultStatus(result) == PGRES_TUPLES_OK &&
+            schemaname != NULL && !verbatim && !objectquoted)
+        {
+            for (int i = 0; i < PQntuples(result); i++)
+            {
+                const char *item = PQgetvalue(result, i, 0);
+
+                if (identifier_needs_quotes(item))
+                {
+                    objectquoted = true;
+                    break;
+                }
+            }
+        }
+
+        /* Clean up */
         termPQExpBuffer(&query_buffer);
-        free(e_text);
+        if (e_schemaname)
+            free(e_schemaname);
+        free(e_objectname);
         if (e_info_charp)
             free(e_info_charp);
         if (e_info_charp2)
             free(e_info_charp2);
     }

-    /* Find something that matches */
+    /* Return the next result, if any, but not if the query failed */
     if (result && PQresultStatus(result) == PGRES_TUPLES_OK)
     {
-        const char *item;
+        int            nskip;
+
+        if (list_index < PQntuples(result))
+        {
+            const char *item = NULL;
+            const char *nsp = NULL;
+
+            if (!PQgetisnull(result, list_index, 0))
+                item = PQgetvalue(result, list_index, 0);
+            if (PQnfields(result) > 1 &&
+                !PQgetisnull(result, list_index, 1))
+                nsp = PQgetvalue(result, list_index, 1);
+            list_index++;

-        while (list_index < PQntuples(result) &&
-               (item = PQgetvalue(result, list_index++, 0)))
-            if (pg_strncasecmp(text, item, byte_length) == 0)
+            if (verbatim)
                 return pg_strdup(item);
+
+            /*
+             * Hack: if we're returning one single schema name, don't let
+             * Readline add a space after it.  Otherwise it'll stop being part
+             * of the completion subject text, which is not what we want.
+             */
+#ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
+            if (item == NULL && nsp != NULL && PQntuples(result) == 1)
+                rl_completion_append_character = '\0';
+#endif
+
+            return requote_identifier(nsp, item, schemaquoted, objectquoted);
+        }
+
+        /*
+         * When the query result is exhausted, check for hard-wired keywords.
+         * These will only be returned if they match the input-so-far,
+         * ignoring case.
+         */
+        nskip = list_index - PQntuples(result);
+        if (schema_query && schema_query->keywords)
+        {
+            const char *const *itemp = schema_query->keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                    return pg_strdup(item);
+            }
+        }
+        if (keywords)
+        {
+            const char *const *itemp = keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                    return pg_strdup(item);
+            }
+        }
     }

-    /* If nothing matches, free the db structure and return null */
+    /* When nothing (else) matches, free the db structure and return null */
     PQclear(result);
     result = NULL;
     return NULL;
@@ -5144,6 +5354,199 @@ escape_string(const char *text)
 }


+/*
+ * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
+ *
+ * This involves splitting off the schema name if present, de-quoting,
+ * and downcasing any unquoted text.  We are a bit laxer than the backend
+ * in that we allow just portions of a name to be quoted --- that's because
+ * psql metacommands have traditionally behaved that way.
+ *
+ * Outputs are a malloc'd schema name (NULL if none), malloc'd object name,
+ * and booleans telling whether any part of the schema and object name was
+ * double-quoted.
+ */
+static void
+parse_identifier(const char *ident,
+                 char **schemaname, char **objectname,
+                 bool *schemaquoted, bool *objectquoted)
+{
+    size_t        buflen = strlen(ident) + 1;
+    bool        enc_is_single_byte = (pg_encoding_max_length(pset.encoding) == 1);
+    char       *sname;
+    char       *oname;
+    char       *optr;
+    bool        inquotes;
+
+    /* Initialize, making a certainly-large-enough output buffer */
+    sname = NULL;
+    oname = pg_malloc(buflen);
+    *schemaquoted = *objectquoted = false;
+    /* Scan */
+    optr = oname;
+    inquotes = false;
+    while (*ident)
+    {
+        unsigned char ch = (unsigned char) *ident++;
+
+        if (ch == '"')
+        {
+            if (inquotes && *ident == '"')
+            {
+                /* two quote marks within a quoted identifier = emit quote */
+                *optr++ = '"';
+                ident++;
+            }
+            else
+            {
+                inquotes = !inquotes;
+                *objectquoted = true;
+            }
+        }
+        else if (ch == '.' && !inquotes)
+        {
+            /* Found a schema name, transfer it to sname / *schemaquoted */
+            *optr = '\0';
+            free(sname);        /* drop any catalog name */
+            sname = oname;
+            oname = pg_malloc(buflen);
+            optr = oname;
+            *schemaquoted = *objectquoted;
+            *objectquoted = false;
+        }
+        else
+        {
+            if (!inquotes)
+            {
+                /*
+                 * This downcasing transformation should match the backend's
+                 * downcase_identifier() as best we can.  We do not know the
+                 * backend's locale, though, so it's necessarily approximate.
+                 * We assume that psql is operating in the same locale and
+                 * encoding as the backend.
+                 */
+                if (ch >= 'A' && ch <= 'Z')
+                    ch += 'a' - 'A';
+                else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch))
+                    ch = tolower(ch);
+            }
+            *optr++ = (char) ch;
+        }
+    }
+
+    *optr = '\0';
+    *schemaname = sname;
+    *objectname = oname;
+}
+
+
+/*
+ * requote_identifier - Reconstruct a possibly-schema-qualified SQL identifier.
+ *
+ * Build a malloc'd string containing the identifier, with quoting applied
+ * as necessary.  This is more or less the inverse of parse_identifier;
+ * in particular, if an input component was quoted, we'll quote the output
+ * even when that isn't strictly required.
+ *
+ * Unlike parse_identifier, we handle the case where a schema and no
+ * object name is provided, producing just "schema.".
+ */
+static char *
+requote_identifier(const char *schemaname, const char *objectname,
+                   bool quote_schema, bool quote_object)
+{
+    char       *result;
+    size_t        buflen = 1;        /* count the trailing \0 */
+    char       *ptr;
+
+    /*
+     * We could use PQescapeIdentifier for some of this, but not all, and it
+     * adds more notational cruft than it seems worth.
+     */
+    if (schemaname)
+    {
+        buflen += strlen(schemaname) + 1;    /* +1 for the dot */
+        if (!quote_schema)
+            quote_schema = identifier_needs_quotes(schemaname);
+        if (quote_schema)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = schemaname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    if (objectname)
+    {
+        buflen += strlen(objectname);
+        if (!quote_object)
+            quote_object = identifier_needs_quotes(objectname);
+        if (quote_object)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = objectname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    result = pg_malloc(buflen);
+    ptr = result;
+    if (schemaname)
+    {
+        if (quote_schema)
+            *ptr++ = '"';
+        for (const char *p = schemaname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_schema)
+            *ptr++ = '"';
+        *ptr++ = '.';
+    }
+    if (objectname)
+    {
+        if (quote_object)
+            *ptr++ = '"';
+        for (const char *p = objectname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_object)
+            *ptr++ = '"';
+    }
+    *ptr = '\0';
+    return result;
+}
+
+
+/*
+ * Detect whether an identifier must be double-quoted.
+ *
+ * Note we'll quote anything that's not ASCII; the backend's quote_ident()
+ * does the same.
+ */
+static bool
+identifier_needs_quotes(const char *ident)
+{
+    if (!((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'))
+        return true;
+    if (strspn(ident, "abcdefghijklmnopqrstuvwxyz0123456789_") != strlen(ident))
+        return true;
+
+    /* XXX is it worth checking for SQL reserved words? */
+
+    return false;
+}
+
+
 /*
  * Execute a query and report any errors. This should be the preferred way of
  * talking to the database in this file.
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 27a0ecfaf4..677ff43e37 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -109,6 +109,12 @@ typedef struct VersionedQuery
  * time.  So we put the components of each query into this struct and
  * assemble them with the common boilerplate in _complete_from_query().
  *
+ * We also use this struct to define queries that use completion_info_object,
+ * which is some object related to the one(s) we want to get the names of
+ * (for example, the table we want the indexes of).  In that usage the
+ * objects we're completing might not have a schema of their own, but the
+ * related object almost always does (passed in completion_info_schema).
+ *
  * As with VersionedQuery, we can use an array of these if the query details
  * must vary across versions.
  */
@@ -122,8 +128,9 @@ typedef struct SchemaQuery
     int            min_server_version;

     /*
-     * Name of catalog or catalogs to be queried, with alias, eg.
-     * "pg_catalog.pg_class c".  Note that "pg_namespace n" will be added.
+     * Name of catalog or catalogs to be queried, with alias(es), eg.
+     * "pg_catalog.pg_class c".  Note that "pg_namespace n" and/or
+     * "pg_namespace ni" will be added automatically when needed.
      */
     const char *catname;

@@ -139,27 +146,60 @@ typedef struct SchemaQuery
     /*
      * Visibility condition --- which rows are visible without schema
      * qualification?  For example, "pg_catalog.pg_table_is_visible(c.oid)".
+     * NULL if not needed.
      */
     const char *viscondition;

     /*
-     * Namespace --- name of field to join to pg_namespace.oid. For example,
-     * "c.relnamespace".
+     * Namespace --- name of field to join to pg_namespace.oid when there is
+     * schema qualification.  For example, "c.relnamespace".  NULL if we don't
+     * want to join to pg_namespace (then any schema part in the input word
+     * will be ignored).
      */
     const char *namespace;

     /*
-     * Result --- the (unquoted) name to return, in the case of an unqualified
-     * name.  For example, "c.relname".
+     * Result --- the base object name to return.  For example, "c.relname".
      */
     const char *result;

+    /*
+     * In some cases, it's difficult to keep the query from returning the same
+     * object multiple times.  Specify use_distinct to filter out duplicates.
+     */
+    bool        use_distinct;
+
     /*
      * Additional literal strings (usually keywords) to be offered along with
      * the query results.  Provide a NULL-terminated array of constant
      * strings, or NULL if none.
      */
     const char *const *keywords;
+
+    /*
+     * If this query uses completion_info_object/completion_info_schema,
+     * populate the remaining fields, else leave them NULL.  When using this
+     * capability, catname must include the catalog that defines the
+     * completion_info_object, and selcondition must include the join
+     * condition that connects it to the result's catalog.
+     *
+     * infoname is the field that should be equated to completion_info_object,
+     * for example "ci.relname".
+     */
+    const char *infoname;
+
+    /*
+     * Visibility condition to use when completion_info_schema is not set. For
+     * example, "pg_catalog.pg_table_is_visible(ci.oid)".  NULL if not needed.
+     */
+    const char *infoviscondition;
+
+    /*
+     * Name of field to join to pg_namespace.oid when completion_info_schema
+     * is set.  For example, "ci.relnamespace".  NULL if we don't want to
+     * consider completion_info_schema.
+     */
+    const char *infonamespace;
 } SchemaQuery;


@@ -176,10 +216,10 @@ static int    completion_max_records;
 static char completion_last_char;    /* last char of input word */
 static const char *completion_charp;    /* to pass a string */
 static const char *const *completion_charpp;    /* to pass a list of strings */
-static const char *completion_info_charp;    /* to pass a second string */
-static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
+static char *completion_info_object;    /* name of a related object */
+static char *completion_info_schema;    /* schema name of a related object */
 static bool completion_case_sensitive;    /* completion is case sensitive */
 static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */
@@ -247,6 +287,7 @@ do { \
 do { \
     completion_squery = &(query); \
     completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

@@ -256,6 +297,14 @@ do { \
     COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
 } while (0)

+#define COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(query) \
+do { \
+    completion_squery = &(query); \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
+} while (0)
+
 #define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
     COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)

@@ -310,29 +359,11 @@ do { \

 #define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_table; \
-\
-    _completion_schema = strtokx(relation, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_table = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                false, false, pset.encoding); \
-    if (_completion_table == NULL) \
-    { \
-        completion_charp = Query_for_list_of_attributes; \
-        completion_info_charp = relation; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_attributes_with_schema; \
-        completion_info_charp = _completion_table; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(relation); \
+    completion_squery = &(Query_for_list_of_attributes); \
     completion_charpp = list; \
     completion_verbatim = false; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
@@ -349,65 +380,24 @@ do { \
  */
 #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); \
-    use_quotes = (text[0] == '\'' || \
-                  start == 0 || rl_line_buffer[start - 1] != '\''); \
-    if (_completion_type == NULL) \
-    { \
-        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; \
-    } \
+    setup_completion_info(type); \
+    if (text[0] == '\'' || \
+        start == 0 || rl_line_buffer[start - 1] != '\'') \
+        completion_squery = &(Query_for_list_of_enum_values_quoted); \
     else \
-    { \
-        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; \
-    } \
+        completion_squery = &(Query_for_list_of_enum_values_unquoted); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_FUNCTION_ARG(function) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_function; \
-\
-    _completion_schema = strtokx(function, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_function = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                   false, false, pset.encoding); \
-    if (_completion_function == NULL) \
-    { \
-        completion_charp = Query_for_list_of_arguments; \
-        completion_info_charp = function; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_arguments_with_schema; \
-        completion_info_charp = _completion_function; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(function); \
+    completion_squery = &(Query_for_list_of_arguments); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 /*
@@ -417,6 +407,51 @@ do { \
  * unnecessary bloat in the completions generated.
  */

+static const SchemaQuery Query_for_constraint_of_table = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_table_not_validated = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid and not con.convalidated",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_type = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_type t",
+    .selcondition = "con.contypid=t.oid",
+    .result = "con.conname",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_unique_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid and i.indisunique",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
 static const SchemaQuery Query_for_list_of_aggregates[] = {
     {
         .min_server_version = 110000,
@@ -435,6 +470,32 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
     }
 };

+static const SchemaQuery Query_for_list_of_arguments = {
+    .catname = "pg_catalog.pg_proc p",
+    .result = "pg_catalog.oidvectortypes(p.proargtypes)||')'",
+    .infoname = "p.proname",
+    .infoviscondition = "pg_catalog.pg_function_is_visible(p.oid)",
+    .infonamespace = "p.pronamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attributes = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attname",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attribute_numbers = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attnum::pg_catalog.text",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
 static const char *const Keywords_for_list_of_datatypes[] = {
     "bigint",
     "boolean",
@@ -495,6 +556,24 @@ static const SchemaQuery Query_for_list_of_domains = {
     .result = "t.typname",
 };

+static const SchemaQuery Query_for_list_of_enum_values_quoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "pg_catalog.quote_literal(enumlabel)",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_enum_values_unquoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "e.enumlabel",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
 /* Note: this intentionally accepts aggregates as well as plain functions */
 static const SchemaQuery Query_for_list_of_functions[] = {
     {
@@ -569,6 +648,48 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .result = "c.relname",
 };

+static const SchemaQuery Query_for_list_of_tables_for_constraint = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_constraint con",
+    .selcondition = "c.oid=con.conrelid and c.relkind IN ("
+    CppAsString2(RELKIND_RELATION) ", "
+    CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "con.conname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_policy = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_policy p",
+    .selcondition = "c.oid=p.polrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "p.polname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_rule = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_rewrite r",
+    .selcondition = "c.oid=r.ev_class",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "r.rulename",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_trigger = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_trigger t",
+    .selcondition = "c.oid=t.tgrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "t.tgname",
+};
+
 static const SchemaQuery Query_for_list_of_views = {
     .catname = "pg_catalog.pg_class c",
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
@@ -717,7 +838,6 @@ static const SchemaQuery Query_for_list_of_clusterables = {
 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .catname = "pg_catalog.pg_constraint c",
     .selcondition = "c.conrelid <> 0",
-    .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
     .result = "c.conname",
 };
@@ -737,96 +857,56 @@ static const SchemaQuery Query_for_list_of_collations = {
     .result = "c.collname",
 };

+static const SchemaQuery Query_for_partition_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
+    .selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
+    .viscondition = "pg_catalog.pg_table_is_visible(c2.oid)",
+    .namespace = "c2.relnamespace",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_rule_of_table = {
+    .catname = "pg_catalog.pg_rewrite r, pg_catalog.pg_class c1",
+    .selcondition = "r.ev_class=c1.oid",
+    .result = "r.rulename",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_trigger_of_table = {
+    .catname = "pg_catalog.pg_trigger t, pg_catalog.pg_class c1",
+    .selcondition = "t.tgrelid=c1.oid and not t.tgisinternal",
+    .result = "t.tgname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+

 /*
  * Queries to get lists of names of various kinds of things, possibly
- * restricted to names matching a partially entered name.  In these queries,
- * the first %s will be replaced by the text entered so far (suitably escaped
- * to become a SQL literal string).  %d will be replaced by the length of the
- * string (in unescaped form).  A second and third %s, if present, will be
- * replaced by a suitably-escaped version of the string provided in
- * completion_info_charp.  A fourth and fifth %s are similarly replaced by
- * completion_info_charp2.
+ * restricted to names matching a partially entered name.  Don't use
+ * this method where the user might wish to enter a schema-qualified
+ * name; make a SchemaQuery instead.
+ *
+ * In these queries, there must be a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
  *
- * Beware that the allowed sequences of %s and %d are determined by
- * _complete_from_query().
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  NOTE: using completion_info_object
+ * that way is usually the wrong thing, and using completion_info_schema
+ * that way is always the wrong thing.  Make a SchemaQuery instead.
  */

-#define Query_for_list_of_attributes \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attribute_numbers \
-"SELECT attnum "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attributes_with_schema \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
-" WHERE c.oid = a.attrelid "\
-"   AND n.oid = c.relnamespace "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
-#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 "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND pg_catalog.pg_type_is_visible(t.oid)"
-
-#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 (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 "\
-"   AND n.oid = t.typnamespace "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (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 (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_template_databases \
 "SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
@@ -885,81 +965,12 @@ static const SchemaQuery Query_for_list_of_collations = {
 #define Keywords_for_list_of_grant_roles \
 "PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-#define Query_for_index_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
-" WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"
-
-#define Query_for_unique_index_of_table \
-Query_for_index_of_table \
-"       and i.indisunique"
-
-#define Query_for_constraint_of_table \
-"SELECT conname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_constraint_of_table_not_validated \
-Query_for_constraint_of_table \
-"       and not con.convalidated"
-
 #define Query_for_all_table_constraints \
 "SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "\
 "       and substring(conname,1,%d)='%s'"

-#define Query_for_constraint_of_type \
-"SELECT conname "\
-"  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
-"       and t.typname='%s'"\
-"       and pg_catalog.pg_type_is_visible(t.oid)"
-
-#define Query_for_list_of_tables_for_constraint \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE conname='%s')"
-
-#define Query_for_rule_of_table \
-"SELECT rulename "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_list_of_tables_for_rule \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE rulename='%s')"
-
-#define Query_for_trigger_of_table \
-"SELECT tgname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"\
-"       and not tgisinternal"
-
-#define Query_for_list_of_tables_for_trigger \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE tgname='%s')"
-
 #define Query_for_list_of_ts_configurations \
 "SELECT cfgname FROM pg_catalog.pg_ts_config "\
 " WHERE substring(cfgname,1,%d)='%s'"
@@ -1008,24 +1019,6 @@ Query_for_constraint_of_table \
 "  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-#define Query_for_list_of_arguments \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND (proname='%s'"\
-"        OR '\"' || proname || '\"'='%s') "\
-"   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"
-
-#define Query_for_list_of_arguments_with_schema \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND n.oid = p.pronamespace "\
-"   AND (proname='%s' "\
-"        OR '\"' || proname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_extensions \
 " SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
@@ -1072,30 +1065,18 @@ Query_for_constraint_of_table \
 "   FROM pg_catalog.pg_policy "\
 "  WHERE substring(polname,1,%d)='%s'"

-#define Query_for_list_of_tables_for_policy \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE polname='%s')"
-
-#define Query_for_enum \
-" SELECT name FROM ( "\
-"   SELECT pg_catalog.unnest(enumvals) AS name "\
+#define Query_for_values_of_enum_GUC \
+" SELECT val FROM ( "\
+"   SELECT name, pg_catalog.unnest(enumvals) AS val "\
 "     FROM pg_catalog.pg_settings "\
-"    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
 "    ) ss "\
-"  WHERE pg_catalog.substring(name,1,%%d)='%%s'"
+"  WHERE substring(val,1,%d)='%s'"\
+"        and pg_catalog.lower(name)=pg_catalog.lower('%s')"

-#define Query_for_partition_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
-" WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"\
-"       and c2.relispartition = 'true'"
+#define Query_for_list_of_channels \
+" SELECT channel "\
+"   FROM pg_catalog.pg_listening_channels() AS channel "\
+"  WHERE substring(channel,1,%d)='%s'"

 #define Query_for_list_of_cursors \
 " SELECT name "\
@@ -1278,6 +1259,7 @@ static char *_complete_from_query(const char *simple_query,
                                   const char *const *keywords,
                                   bool verbatim,
                                   const char *text, int state);
+static void setup_completion_info(const char *word);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
 static void append_variable_names(char ***varnames, int *nvars,
@@ -1683,8 +1665,10 @@ psql_completion(const char *text, int start, int end)
     /* Clear a few things. */
     completion_charp = NULL;
     completion_charpp = NULL;
-    completion_info_charp = NULL;
-    completion_info_charp2 = NULL;
+    completion_vquery = NULL;
+    completion_squery = NULL;
+    completion_info_object = NULL;
+    completion_info_schema = NULL;

     /*
      * Scan the input line to extract the words before our current position.
@@ -1864,14 +1848,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER EXTENSION <name> UPDATE */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
-        completion_info_charp = prev3_wd;
+        setup_completion_info(prev3_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -1911,8 +1895,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER INDEX <name> ALTER COLUMN */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_attribute_numbers);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(Query_for_list_of_attribute_numbers);
     }
     /* ALTER INDEX <name> ALTER COLUMN <colnum> */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2022,8 +2006,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER DOMAIN <sth> DROP|RENAME|VALIDATE CONSTRAINT */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "DROP|RENAME|VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_type);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_type);
     }
     /* ALTER DOMAIN <sth> RENAME */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "RENAME"))
@@ -2108,8 +2092,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER POLICY <name> ON <table> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     /* ALTER POLICY <name> ON <table> - show options */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny))
@@ -2132,8 +2116,8 @@ psql_completion(const char *text, int start, int end)
     /* If we have ALTER RULE <name> ON, then add the correct tablename */
     else if (Matches("ALTER", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }

     /* ALTER RULE <name> ON <name> */
@@ -2148,18 +2132,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");

-    else if (Matches("ALTER", "TRIGGER", MatchAny, MatchAny))
+    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }

-    /*
-     * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
-     */
-    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-
     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("RENAME TO", "DEPENDS ON EXTENSION",
@@ -2198,28 +2176,28 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE xxx ADD PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev6_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev6_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev8_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev8_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev7_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev7_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ENABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE"))
@@ -2229,23 +2207,23 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "RULE"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "TRIGGER"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
@@ -2261,13 +2239,13 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }

     /* ALTER TABLE xxx ALTER */
@@ -2297,14 +2275,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table);
     }
     /* ALTER TABLE <sth> VALIDATE CONSTRAINT <non-validated constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table_not_validated);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table_not_validated);
     }
     /* ALTER TABLE ALTER [COLUMN] <foo> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny) ||
@@ -2336,8 +2314,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("ALTER", "TABLE", MatchAny, "CLUSTER", "ON"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     /* If we have ALTER TABLE <sth> SET, provide list of attributes and '(' */
     else if (Matches("ALTER", "TABLE", MatchAny, "SET"))
@@ -2368,8 +2346,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_LIST(table_storage_parameters);
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING"))
         COMPLETE_WITH("INDEX");
@@ -2396,8 +2374,8 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_partition_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
         COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
@@ -2534,8 +2512,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CLUSTER", MatchAny, "USING") ||
              Matches("CLUSTER", "VERBOSE|(*)", MatchAny, "USING"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (HeadMatches("CLUSTER", "(*") &&
              !HeadMatches("CLUSTER", "(*)"))
@@ -2572,9 +2550,9 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
-                                 "DOMAIN");
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                        "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
@@ -2592,8 +2570,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("COMMENT", "ON", "PROCEDURAL", "LANGUAGE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
@@ -2601,8 +2579,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH"))
         COMPLETE_WITH("CONFIGURATION", "DICTIONARY", "PARSER", "TEMPLATE");
@@ -2620,15 +2598,15 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("COMMENT", "ON", MatchAny, MatchAnyExcept("IS")) ||
              Matches("COMMENT", "ON", MatchAny, MatchAny, MatchAnyExcept("IS")) ||
@@ -2744,7 +2722,7 @@ psql_completion(const char *text, int start, int end)
     /* CREATE EXTENSION <name> VERSION */
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -3055,7 +3033,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }

@@ -3478,8 +3456,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3505,8 +3483,8 @@ psql_completion(const char *text, int start, int end)
     /* DROP POLICY <name> ON <table> */
     else if (Matches("DROP", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("DROP", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3516,8 +3494,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3531,7 +3509,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
@@ -3915,7 +3893,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");
+        COMPLETE_WITH_QUERY(Query_for_list_of_channels);

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -4151,11 +4129,9 @@ psql_completion(const char *text, int start, int end)
             {
                 if (strcmp(guctype, "enum") == 0)
                 {
-                    char        querybuf[1024];
-
-                    snprintf(querybuf, sizeof(querybuf),
-                             Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
+                    setup_completion_info(prev2_wd);
+                    COMPLETE_WITH_QUERY_PLUS(Query_for_values_of_enum_GUC,
+                                             "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4200,9 +4176,7 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
-                                 " WHERE substring(channel,1,%d)='%s'",
-                                 "*");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_channels, "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
@@ -4403,7 +4377,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_encodings);
     else if (TailMatchesCS("\\h|\\help"))
         COMPLETE_WITH_LIST(sql_commands);
     else if (TailMatchesCS("\\h|\\help", MatchAny))
@@ -4562,6 +4536,12 @@ psql_completion(const char *text, int start, int end)
     free(previous_words);
     free(words_buffer);
     free(text_copy);
+    if (completion_info_object)
+        free(completion_info_object);
+    completion_info_object = NULL;
+    if (completion_info_schema)
+        free(completion_info_schema);
+    completion_info_schema = NULL;

     /* Return our Grand List O' Matches */
     return matches;
@@ -4673,7 +4653,7 @@ complete_from_schema_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
     return _complete_from_query(NULL, completion_squery, completion_charpp,
-                                false, text, state);
+                                completion_verbatim, text, state);
 }

 static char *
@@ -4705,10 +4685,11 @@ complete_from_versioned_schema_query(const char *text, int state)
  * where "OUTPUT" is the same string that the query returns.  The %d and %s
  * will be replaced by the string length of the text and the text itself,
  * causing the results to be limited to those matching the already-typed text.
- * The query may also have up to four more %s in it; the first two such will
- * be replaced by the value of completion_info_charp, the next two by the
- * value of completion_info_charp2.  (These strings will be escaped to
- * become SQL literals, so what is actually in the query should be '%s'.)
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  Those strings should be set up
+ * by calling setup_completion_info().
  * Simple queries should return a single column of matches.  If "verbatim"
  * is true, the matches are returned as-is; otherwise, they are taken to
  * be SQL identifiers and quoted if necessary.
@@ -4759,8 +4740,8 @@ _complete_from_query(const char *simple_query,
         int            object_length = 0;
         char       *e_schemaname;
         char       *e_objectname;
-        char       *e_info_charp;
-        char       *e_info_charp2;
+        char       *e_info_object;
+        char       *e_info_schema;

         /* Reset static state, ensuring no memory leaks */
         list_index = 0;
@@ -4804,15 +4785,15 @@ _complete_from_query(const char *simple_query,

         e_objectname = escape_string(objectname);

-        if (completion_info_charp)
-            e_info_charp = escape_string(completion_info_charp);
+        if (completion_info_object)
+            e_info_object = escape_string(completion_info_object);
         else
-            e_info_charp = NULL;
+            e_info_object = NULL;

-        if (completion_info_charp2)
-            e_info_charp2 = escape_string(completion_info_charp2);
+        if (completion_info_schema)
+            e_info_schema = escape_string(completion_info_schema);
         else
-            e_info_charp2 = NULL;
+            e_info_schema = NULL;

         initPQExpBuffer(&query_buffer);

@@ -4825,20 +4806,44 @@ _complete_from_query(const char *simple_query,
              * already qualified or not.  schema_query gives us the pieces to
              * assemble.
              */
-            if (schemaname == NULL)
+            if (schemaname == NULL || schema_query->namespace == NULL)
             {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer,
+                              "%s, NULL::pg_catalog.text FROM %s",
                               schema_query->result,
                               schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBufferStr(&query_buffer, " WHERE ");
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
                               object_length, e_objectname);
+            if (schema_query->viscondition)
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }

             /*
              * When fetching relation names, suppress system catalogs unless
@@ -4855,7 +4860,12 @@ _complete_from_query(const char *simple_query,
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

-            /* Add in schema names matching the input-so-far */
+            /*
+             * If the target object type can be schema-qualified, add in
+             * schema names matching the input-so-far.
+             */
+            if (schema_query->namespace)
+            {
             appendPQExpBuffer(&query_buffer, "\nUNION\n"
                               "SELECT NULL::pg_catalog.text, n.nspname "
                               "FROM pg_catalog.pg_namespace n "
@@ -4876,14 +4886,21 @@ _complete_from_query(const char *simple_query,
              */
             schemaquoted = objectquoted;
             }
+            }
             else
             {
             /* Input is qualified, so produce only qualified names */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n "
-                              "WHERE %s = n.oid AND ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                              "FROM %s, pg_catalog.pg_namespace n",
                               schema_query->result,
-                              schema_query->catname,
+                              schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
@@ -4893,6 +4910,21 @@ _complete_from_query(const char *simple_query,
                               object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
                               e_schemaname);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }
             }
         }
         else
@@ -4901,8 +4933,7 @@ _complete_from_query(const char *simple_query,
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
                               object_length, e_objectname,
-                              e_info_charp, e_info_charp,
-                              e_info_charp2, e_info_charp2);
+                              e_info_object, e_info_schema);
         }

         /* Limit the number of records in the result */
@@ -4924,7 +4955,8 @@ _complete_from_query(const char *simple_query,
          * if dealing with unqualified names.
          */
         if (result && PQresultStatus(result) == PGRES_TUPLES_OK &&
-            schemaname != NULL && !verbatim && !objectquoted)
+            schemaname != NULL && schema_query->namespace != NULL &&
+            !verbatim && !objectquoted)
         {
             for (int i = 0; i < PQntuples(result); i++)
             {
@@ -4943,10 +4975,10 @@ _complete_from_query(const char *simple_query,
         if (e_schemaname)
             free(e_schemaname);
         free(e_objectname);
-        if (e_info_charp)
-            free(e_info_charp);
-        if (e_info_charp2)
-            free(e_info_charp2);
+        if (e_info_object)
+            free(e_info_object);
+        if (e_info_schema)
+            free(e_info_schema);
     }

     /* Return the next result, if any, but not if the query failed */
@@ -5027,6 +5059,23 @@ _complete_from_query(const char *simple_query,
 }


+/*
+ * Set up completion_info_object and completion_info_schema
+ * by parsing the given word.  These variables can then be
+ * used in a query passed to _complete_from_query.
+ */
+static void
+setup_completion_info(const char *word)
+{
+    bool        schemaquoted,
+                objectquoted;
+
+    parse_identifier(word,
+                     &completion_info_schema, &completion_info_object,
+                     &schemaquoted, &objectquoted);
+}
+
+
 /*
  * This function returns in order one of a fixed, NULL pointer terminated list
  * of strings (if matching). This can be used if there are only a fixed number
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 677ff43e37..601b108aa4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4808,123 +4808,124 @@ _complete_from_query(const char *simple_query,
              */
             if (schemaname == NULL || schema_query->namespace == NULL)
             {
-            /* Get unqualified names matching the input-so-far */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer,
-                              "%s, NULL::pg_catalog.text FROM %s",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBufferStr(&query_buffer, " WHERE ");
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
-                              schema_query->result,
-                              object_length, e_objectname);
-            if (schema_query->viscondition)
-            appendPQExpBuffer(&query_buffer, " AND %s",
-                              schema_query->viscondition);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Get unqualified names matching the input-so-far */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer,
+                                  "%s, NULL::pg_catalog.text FROM %s",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
-
-            /*
-             * When fetching relation names, suppress system catalogs unless
-             * the input-so-far begins with "pg_".  This is a compromise
-             * between not offering system catalogs for completion at all, and
-             * having them swamp the result when the input is just "p".
-             */
-            if (strcmp(schema_query->catname,
-                       "pg_catalog.pg_class c") == 0 &&
-                strncmp(objectname, "pg_", 3) != 0)
-            {
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND c.relnamespace <> (SELECT oid FROM"
-                                     " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
-            }
-
-            /*
-             * If the target object type can be schema-qualified, add in
-             * schema names matching the input-so-far.
-             */
-            if (schema_query->namespace)
-            {
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT NULL::pg_catalog.text, n.nspname "
-                              "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(n.nspname,1,%d)='%s'",
-                              object_length, e_objectname);
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBufferStr(&query_buffer, " WHERE ");
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                if (schema_query->viscondition)
+                    appendPQExpBuffer(&query_buffer, " AND %s",
+                                      schema_query->viscondition);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
 
-            /*
-             * Likewise, suppress system schemas unless the input-so-far
-             * begins with "pg_".
-             */
-            if (strncmp(objectname, "pg_", 3) != 0)
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND n.nspname NOT LIKE 'pg\\_%'");
+                /*
+                 * When fetching relation names, suppress system catalogs
+                 * unless the input-so-far begins with "pg_".  This is a
+                 * compromise between not offering system catalogs for
+                 * completion at all, and having them swamp the result when
+                 * the input is just "p".
+                 */
+                if (strcmp(schema_query->catname,
+                           "pg_catalog.pg_class c") == 0 &&
+                    strncmp(objectname, "pg_", 3) != 0)
+                {
+                    appendPQExpBufferStr(&query_buffer,
+                                         " AND c.relnamespace <> (SELECT oid FROM"
+                                         " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
+                }
 
-            /*
-             * If the input-so-far had quotes, force any added schema names
-             * to be quoted, too.
-             */
-            schemaquoted = objectquoted;
-            }
+                /*
+                 * If the target object type can be schema-qualified, add in
+                 * schema names matching the input-so-far.
+                 */
+                if (schema_query->namespace)
+                {
+                    appendPQExpBuffer(&query_buffer, "\nUNION\n"
+                                      "SELECT NULL::pg_catalog.text, n.nspname "
+                                      "FROM pg_catalog.pg_namespace n "
+                                      "WHERE substring(n.nspname,1,%d)='%s'",
+                                      object_length, e_objectname);
+
+                    /*
+                     * Likewise, suppress system schemas unless the
+                     * input-so-far begins with "pg_".
+                     */
+                    if (strncmp(objectname, "pg_", 3) != 0)
+                        appendPQExpBufferStr(&query_buffer,
+                                             " AND n.nspname NOT LIKE 'pg\\_%'");
+
+                    /*
+                     * If the input-so-far had quotes, force any added schema
+                     * names to be quoted, too.
+                     */
+                    schemaquoted = objectquoted;
+                }
             }
             else
             {
-            /* Input is qualified, so produce only qualified names */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
-                              schema_query->namespace);
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
-                              schema_query->result,
-                              object_length, e_objectname);
-            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
-                              e_schemaname);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Input is qualified, so produce only qualified names */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                                  "FROM %s, pg_catalog.pg_namespace n",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
+                                  schema_query->namespace);
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                                  e_schemaname);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
             }
         }
         else
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index d3d1bd650e..45ad13e762 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -40,10 +40,11 @@ $node->start;

 # set up a few database objects
 $node->safe_psql('postgres',
-        "CREATE TABLE tab1 (f1 int, f2 text);\n"
+        "CREATE TABLE tab1 (f1 int primary key, f2 text);\n"
       . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
       . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
-      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz');\n");
+      . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n"
+      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz', 'BLACK');\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.
@@ -176,6 +177,38 @@ check_completion("2\t", qr/246 /,

 clear_query();

+# check handling of quoted names
+check_completion(
+    "select * from \"my\t",
+    qr/select \* from "my\a?tab/,
+    "complete \"my<tab> to \"mytab when there are multiple choices");
+
+check_completion(
+    "\t\t",
+    qr/"mytab123" +"mytab246"/,
+    "offer multiple quoted table choices");
+
+check_completion("2\t", qr/246" /,
+    "finish completion of one of multiple quoted table choices");
+
+clear_query();
+
+# check handling of mixed-case names
+check_completion(
+    "select * from mi\t",
+    qr/"mixedName"/,
+    "complete a mixed-case name without help");
+
+clear_query();
+
+# check case folding
+check_completion(
+    "select * from TAB\t",
+    qr/tab1 /,
+    "automatically fold case");
+
+clear_query();
+
 # check case-sensitive keyword replacement
 # note: various versions of readline/libedit handle backspacing
 # differently, so just check that the replacement comes out correctly
@@ -183,6 +216,48 @@ check_completion("\\DRD\t", qr/drds /, "complete \\DRD<tab> to \\drds");

 clear_query();

+# check completion of a schema-qualified name
+check_completion(
+    "select * from pub\t",
+    qr/public\./,
+    "complete schema when relevant");
+
+check_completion(
+    "tab\t",
+    qr/tab1 /,
+    "complete schema-qualified name");
+
+clear_query();
+
+check_completion(
+    "select * from PUBLIC.t\t",
+    qr/public\.tab1 /,
+    "automatically fold case in schema-qualified name");
+
+clear_query();
+
+# check interpretation of referenced names
+check_completion(
+    "alter table tab1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table");
+
+clear_query();
+
+check_completion(
+    "alter table TAB1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with downcasing");
+
+clear_query();
+
+check_completion(
+    "alter table public.\"tab1\" drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with schema and quoting");
+
+clear_query();
+
 # check filename completion
 check_completion(
     "\\lo_import tmp_check/some\t",
@@ -234,6 +309,14 @@ check_completion(

 clear_line();

+# enum labels are case sensitive, so this should complete BLACK immediately
+check_completion(
+    "ALTER TYPE enum1 RENAME VALUE 'B\t",
+    qr|BLACK|,
+    "enum labels are case sensitive");
+
+clear_line();
+
 # send psql an explicit \q to shut it down, else pty won't close properly
 $timer->start(5);
 $in .= "\\q\n";

RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Friday, January 28, 2022 5:24 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Here's a fleshed-out patch series for this idea.

Thanks for you patch.
I did some tests on it and here are something cases I feel we need to confirm
whether they are suitable.

1) postgres=# create table atest(id int, "iD" int, "ID" int);
2) CREATE TABLE
3) postgres=# alter table atest rename i[TAB]
4) id    "iD"
5) postgres=# alter table atest rename I[TAB]
6) id    "iD"

The tab completion for 5) ignored "ID", is that correct?

7) postgres=# create table "aTest"("myCol" int, mycol int);
8) CREATE TABLE
9) postgres=# alter table a[TAB]
10) ALL IN TABLESPACE  atest              "aTest"
11) postgres=# alter table aT[TAB]  -> atest

I think what we are trying to do is to ease the burden of typing double quote for user.
But in line 11), the tab completion for "alter table aT[TAB]" is attest,
which makes the tab completion output of "aTest" at 10) no value.
Because if user needs to alter table aTest they still needs to
type double quote manually.

Another thing is the inconsistency  of the output result.
12) postgres=# alter table atest rename i[TAB]
13) id    "iD"
14) postgres=# alter table atest rename "i[TAB]
15) "id"  "iD"

By applying the new fix, Line 15 added the output of "id".
I think it's good to keep user input '"' and convenient when using tab completion.
One the other hand, I'm not so comfortable with the output of "iD" in line 13.
If user doesn't type double quote, why we add double quote to the output?
Could we make the output of 13) like below?
12) postgres=# alter table atest rename i[TAB]
??) id  iD

Regards,
Tang




Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
"tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
> I did some tests on it and here are something cases I feel we need to confirm
> whether they are suitable.

> 1) postgres=# create table atest(id int, "iD" int, "ID" int);
> 2) CREATE TABLE
> 3) postgres=# alter table atest rename i[TAB]
> 4) id    "iD"
> 5) postgres=# alter table atest rename I[TAB]
> 6) id    "iD"

> The tab completion for 5) ignored "ID", is that correct?

Perhaps I misunderstood your original complaint, but what I thought
you were unhappy about was that unquoted ID is a legal spelling of
"id" and so I<TAB> ought to be willing to complete that.  These
examples with case variants of the same word are of some interest,
but people aren't really going to create tables with these sorts of
names, so we shouldn't let them drive the design IMO.

Anyway, the existing behavior for these examples is

alter table atest rename i<TAB> --- completes immediately to id
alter table atest rename I<TAB> --- offers nothing

It's certainly arguable that the first case is right as-is and we
shouldn't change it.  I think that could be handled by tweaking my
patch so that it wouldn't offer completions that start with a quote
unless the input word does.  That would also cause I<TAB> to complete
immediately to id, which is arguably fine.

> I think what we are trying to do is to ease the burden of typing double quote for user.

I'm not thinking about it that way at all.  To me, the goal is to make
tab completion do something sensible when presented with legal variant
spellings of a word.  The two cases where it currently fails to do
that are (1) unquoted input that needs to be downcased, and (2) input
that is quoted when it doesn't strictly need to be.

To the extent that we can supply a required quote that the user
failed to type, that's fine, but it's not a primary goal of the patch.
Examples like these make me question whether it's even something we
want; it's resulting in extraneous matches that people might find more
annoying than helpful.  Now I *think* that these aren't realistic
cases and that in real cases adding quotes will be helpful more often
than not, but it's debatable.

> One the other hand, I'm not so comfortable with the output of "iD" in line 13.
> If user doesn't type double quote, why we add double quote to the output?

That's certainly a valid argument.

> Could we make the output of 13) like below?
> 12) postgres=# alter table atest rename i[TAB]
> ??) id  iD

That doesn't seem sensible at all.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
I wrote:
> It's certainly arguable that the first case is right as-is and we
> shouldn't change it.  I think that could be handled by tweaking my
> patch so that it wouldn't offer completions that start with a quote
> unless the input word does.  That would also cause I<TAB> to complete
> immediately to id, which is arguably fine.

Here's a patch series that does it like that.  I have to admit that
after playing with it, this is probably better.  There's less
magic-looking behavior involved, and it lets me drop an ugly hack
I had to work around a case where Readline didn't want to play along.

0001 also cleans up one oversight in the previous version, which
is to beware of multibyte characters in parse_identifier().  I'm
not sure there is any actual hazard there, since we weren't looking
for backslashes, but it's better to be sure.  I added the keyword
handling I'd left out before, too.

0002-0004 are largely as before.

I've also added 0005, which changes the prefix-matching clauses
in the SQL queries from "substring(foo,1,%d)='%s'" to
"foo LIKE '%s'".  This simplifies reading the queries a little bit,
but the real reason to do it is that the planner can optimize the
catalog searches a lot better.  It knows a lot about LIKE prefix
queries and exactly nothing about substring().  For example,
DROP TYPE foo<TAB> now produces a query like this:

explain SELECT t.typname, NULL::pg_catalog.text FROM pg_catalog.pg_type t WHERE (t.typrelid = 0  OR (SELECT c.relkind =
'c'    FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid)) AND t.typname !~ '^_' AND (t.typname) LIKE 'foo%' AND
pg_catalog.pg_type_is_visible(t.oid);
                                                                QUERY PLAN
                  

------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using pg_type_typname_nsp_index on pg_type t  (cost=0.28..16.63 rows=1 width=96)
   Index Cond: ((typname >= 'foo'::text) AND (typname < 'fop'::text))
   Filter: ((typname !~ '^_'::text) AND (typname ~~ 'foo%'::text) AND pg_type_is_visible(oid) AND ((typrelid =
'0'::oid)OR (SubPlan 1))) 
   SubPlan 1
     ->  Index Scan using pg_class_oid_index on pg_class c  (cost=0.28..8.30 rows=1 width=1)
           Index Cond: (oid = t.typrelid)
(6 rows)

where before you got a seqscan:

explain SELECT pg_catalog.format_type(t.oid, NULL) FROM pg_catalog.pg_type t WHERE (t.typrelid = 0  OR (SELECT
c.relkind= 'c'     FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid)) AND t.typname !~ '^_' AND
substring(pg_catalog.format_type(t.oid,NULL),1,3)='foo' AND pg_catalog.pg_type_is_visible(t.oid); 
    QUERY PLAN

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pg_type t  (cost=0.00..16691.86 rows=1 width=32)
   Filter: ((typname !~ '^_'::text) AND ("substring"(format_type(oid, NULL::integer), 1, 3) = 'foo'::text) AND
pg_type_is_visible(oid)AND ((typrelid = '0'::oid) OR (SubPlan 1))) 
   SubPlan 1
     ->  Index Scan using pg_class_oid_index on pg_class c  (cost=0.28..8.30 rows=1 width=1)
           Index Cond: (oid = t.typrelid)
(5 rows)

Again, while these queries only have to run at human speed, that doesn't
mean it's okay to be wasteful.  I seem to recall hearing complaints that
they are noticeably slow in installations with many thousand tables, too.
This should help.

            regards, tom lane

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4c62e7b1b4..16999dd9a2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -46,7 +46,9 @@
 #include "catalog/pg_am_d.h"
 #include "catalog/pg_class_d.h"
 #include "common.h"
+#include "common/keywords.h"
 #include "libpq-fe.h"
+#include "mb/pg_wchar.h"
 #include "pqexpbuffer.h"
 #include "settings.h"
 #include "stringutils.h"
@@ -148,16 +150,16 @@ typedef struct SchemaQuery
     const char *namespace;

     /*
-     * Result --- the appropriately-quoted name to return, in the case of an
-     * unqualified name.  For example, "pg_catalog.quote_ident(c.relname)".
+     * Result --- the base object name to return.  For example, "c.relname".
      */
     const char *result;

     /*
-     * In some cases a different result must be used for qualified names.
-     * Enter that here, or write NULL if result can be used.
+     * Additional literal strings (usually keywords) to be offered along with
+     * the query results.  Provide a NULL-terminated array of constant
+     * strings, or NULL if none.
      */
-    const char *qualresult;
+    const char *const *keywords;
 } SchemaQuery;


@@ -179,6 +181,7 @@ static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
 static bool completion_case_sensitive;    /* completion is case sensitive */
+static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */

 /*
@@ -190,36 +193,87 @@ static bool completion_force_quote; /* true to force-quote filenames */
  *      We support both simple and versioned schema queries.
  * 3) The items from a null-pointer-terminated list (with or without
  *      case-sensitive comparison); if the list is constant you can build it
- *      with COMPLETE_WITH() or COMPLETE_WITH_CS().
+ *      with COMPLETE_WITH() or COMPLETE_WITH_CS().  The QUERY_LIST and
+ *      QUERY_PLUS forms combine such literal lists with a query result.
  * 4) The list of attributes of the given table (possibly schema-qualified).
  * 5) The list of arguments to the given function (possibly schema-qualified).
  */
 #define COMPLETE_WITH_QUERY(query) \
+    COMPLETE_WITH_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_QUERY_LIST(query, list) \
+do { \
+    completion_charp = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
+    matches = rl_completion_matches(text, complete_from_query); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_VERBATIM(query) \
 do { \
     completion_charp = query; \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

 #define COMPLETE_WITH_VERSIONED_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list) \
 do { \
     completion_vquery = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_versioned_query); \
 } while (0)

-#define COMPLETE_WITH_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_VERSIONED_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = &(query); \
-    completion_charp = addon; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

-#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = query; \
-    completion_vquery = addon; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_versioned_schema_query); \
 } while (0)

+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
 /*
  * Caution: COMPLETE_WITH_CONST is not for general-purpose use; you probably
  * want COMPLETE_WITH() with one element, instead.
@@ -253,7 +307,10 @@ do { \
     COMPLETE_WITH_LIST_CS(list); \
 } while (0)

-#define COMPLETE_WITH_ATTR(relation, addon) \
+#define COMPLETE_WITH_ATTR(relation) \
+    COMPLETE_WITH_ATTR_LIST(relation, NULL)
+
+#define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
     char   *_completion_schema; \
     char   *_completion_table; \
@@ -266,18 +323,26 @@ do { \
                                 false, false, pset.encoding); \
     if (_completion_table == NULL) \
     { \
-        completion_charp = Query_for_list_of_attributes  addon; \
+        completion_charp = Query_for_list_of_attributes; \
         completion_info_charp = relation; \
     } \
     else \
     { \
-        completion_charp = Query_for_list_of_attributes_with_schema  addon; \
+        completion_charp = Query_for_list_of_attributes_with_schema; \
         completion_info_charp = _completion_table; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

+#define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_ATTR_LIST(relation, list); \
+} while (0)
+
 /*
  * libedit will typically include the literal's leading single quote in
  * "text", while readline will not.  Adapt our offered strings to fit.
@@ -315,6 +380,8 @@ do { \
         completion_info_charp = _completion_type; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -340,6 +407,8 @@ do { \
         completion_info_charp = _completion_function; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -357,17 +426,45 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
         .selcondition = "p.prokind = 'a'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .selcondition = "p.proisagg",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

+static const char *const Keywords_for_list_of_datatypes[] = {
+    "bigint",
+    "boolean",
+    "character",
+    "double precision",
+    "integer",
+    "real",
+    "smallint",
+
+    /*
+     * Note: currently there's no value in offering the following multiword
+     * type names, because tab completion cannot succeed for them: we can't
+     * disambiguate until somewhere in the second word, at which point we
+     * won't have the first word as context.  ("double precision" does work,
+     * as long as no other type name begins with "double".)  Leave them out to
+     * encourage users to use the PG-specific aliases, which we can complete.
+     */
+#ifdef NOT_USED
+    "bit varying",
+    "character varying",
+    "time with time zone",
+    "time without time zone",
+    "timestamp with time zone",
+    "timestamp without time zone",
+#endif
+    NULL
+};
+
 static const SchemaQuery Query_for_list_of_datatypes = {
     .catname = "pg_catalog.pg_type t",
     /* selcondition --- ignore table rowtypes and array types */
@@ -377,8 +474,8 @@ static const SchemaQuery Query_for_list_of_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
+    .keywords = Keywords_for_list_of_datatypes,
 };

 static const SchemaQuery Query_for_list_of_composite_datatypes = {
@@ -389,8 +486,7 @@ static const SchemaQuery Query_for_list_of_composite_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 static const SchemaQuery Query_for_list_of_domains = {
@@ -398,7 +494,7 @@ static const SchemaQuery Query_for_list_of_domains = {
     .selcondition = "t.typtype = 'd'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 /* Note: this intentionally accepts aggregates as well as plain functions */
@@ -409,13 +505,13 @@ static const SchemaQuery Query_for_list_of_functions[] = {
         .selcondition = "p.prokind != 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

@@ -426,7 +522,7 @@ static const SchemaQuery Query_for_list_of_procedures[] = {
         .selcondition = "p.prokind = 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         /* not supported in older versions */
@@ -438,7 +534,7 @@ static const SchemaQuery Query_for_list_of_routines = {
     .catname = "pg_catalog.pg_proc p",
     .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
     .namespace = "p.pronamespace",
-    .result = "pg_catalog.quote_ident(p.proname)",
+    .result = "p.proname",
 };

 static const SchemaQuery Query_for_list_of_sequences = {
@@ -446,7 +542,7 @@ static const SchemaQuery Query_for_list_of_sequences = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_SEQUENCE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_foreign_tables = {
@@ -454,7 +550,7 @@ static const SchemaQuery Query_for_list_of_foreign_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_tables = {
@@ -464,7 +560,7 @@ static const SchemaQuery Query_for_list_of_tables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_tables = {
@@ -472,7 +568,7 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_views = {
@@ -480,7 +576,7 @@ static const SchemaQuery Query_for_list_of_views = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_matviews = {
@@ -488,7 +584,7 @@ static const SchemaQuery Query_for_list_of_matviews = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_indexes = {
@@ -498,7 +594,7 @@ static const SchemaQuery Query_for_list_of_indexes = {
     CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_indexes = {
@@ -506,7 +602,7 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
     .selcondition = "c.relkind = " CppAsString2(RELKIND_PARTITIONED_INDEX),
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };


@@ -515,7 +611,7 @@ static const SchemaQuery Query_for_list_of_relations = {
     .catname = "pg_catalog.pg_class c",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* partitioned relations */
@@ -525,14 +621,14 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = {
     ", " CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_operator_families = {
     .catname = "pg_catalog.pg_opfamily c",
     .viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)",
     .namespace = "c.opfnamespace",
-    .result = "pg_catalog.quote_ident(c.opfname)",
+    .result = "c.opfname",
 };

 /* Relations supporting INSERT, UPDATE or DELETE */
@@ -545,7 +641,7 @@ static const SchemaQuery Query_for_list_of_updatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting SELECT */
@@ -560,7 +656,7 @@ static const SchemaQuery Query_for_list_of_selectables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting TRUNCATE */
@@ -572,7 +668,7 @@ static const SchemaQuery Query_for_list_of_truncatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting GRANT are currently same as those supporting SELECT */
@@ -588,7 +684,7 @@ static const SchemaQuery Query_for_list_of_analyzables = {
     CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting index creation */
@@ -600,7 +696,7 @@ static const SchemaQuery Query_for_list_of_indexables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /*
@@ -617,7 +713,7 @@ static const SchemaQuery Query_for_list_of_clusterables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
@@ -625,14 +721,14 @@ static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .selcondition = "c.conrelid <> 0",
     .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
-    .result = "pg_catalog.quote_ident(c.conname)",
+    .result = "c.conname",
 };

 static const SchemaQuery Query_for_list_of_statistics = {
     .catname = "pg_catalog.pg_statistic_ext s",
     .viscondition = "pg_catalog.pg_statistics_obj_is_visible(s.oid)",
     .namespace = "s.stxnamespace",
-    .result = "pg_catalog.quote_ident(s.stxname)",
+    .result = "s.stxname",
 };

 static const SchemaQuery Query_for_list_of_collations = {
@@ -640,7 +736,7 @@ static const SchemaQuery Query_for_list_of_collations = {
     .selcondition = "c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))",
     .viscondition = "pg_catalog.pg_collation_is_visible(c.oid)",
     .namespace = "c.collnamespace",
-    .result = "pg_catalog.quote_ident(c.collname)",
+    .result = "c.collname",
 };


@@ -659,13 +755,13 @@ static const SchemaQuery Query_for_list_of_collations = {
  */

 #define Query_for_list_of_attributes \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
 " WHERE c.oid = a.attrelid "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

@@ -676,21 +772,21 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
 "   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

 #define Query_for_list_of_attributes_with_schema \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
 " WHERE c.oid = a.attrelid "\
 "   AND n.oid = c.relnamespace "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_quoted \
@@ -698,7 +794,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
 " WHERE t.oid = e.enumtypid "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -707,7 +803,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  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' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -717,9 +813,9 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_with_schema_unquoted \
@@ -728,24 +824,24 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(enumlabel,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_template_databases \
-"SELECT pg_catalog.quote_ident(d.datname) "\
+"SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(pg_catalog.quote_ident(d.datname),1,%d)='%s' "\
+" WHERE substring(d.datname,1,%d)='%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
-"SELECT pg_catalog.quote_ident(datname) FROM pg_catalog.pg_database "\
-" WHERE substring(pg_catalog.quote_ident(datname),1,%d)='%s'"
+"SELECT datname FROM pg_catalog.pg_database "\
+" WHERE substring(datname,1,%d)='%s'"

 #define Query_for_list_of_tablespaces \
-"SELECT pg_catalog.quote_ident(spcname) FROM pg_catalog.pg_tablespace "\
-" WHERE substring(pg_catalog.quote_ident(spcname),1,%d)='%s'"
+"SELECT spcname FROM pg_catalog.pg_tablespace "\
+" WHERE substring(spcname,1,%d)='%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
@@ -753,287 +849,260 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"

 #define Query_for_list_of_languages \
-"SELECT pg_catalog.quote_ident(lanname) "\
+"SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(pg_catalog.quote_ident(lanname),1,%d)='%s'"
+"   AND substring(lanname,1,%d)='%s'"

 #define Query_for_list_of_schemas \
-"SELECT pg_catalog.quote_ident(nspname) FROM pg_catalog.pg_namespace "\
-" WHERE substring(pg_catalog.quote_ident(nspname),1,%d)='%s'"
+"SELECT nspname FROM pg_catalog.pg_namespace "\
+" WHERE substring(nspname,1,%d)='%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context != 'internal' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context IN ('user', 'superuser') "\
-"  UNION ALL SELECT 'constraints' "\
-"  UNION ALL SELECT 'transaction' "\
-"  UNION ALL SELECT 'session' "\
-"  UNION ALL SELECT 'role' "\
-"  UNION ALL SELECT 'tablespace' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
-"  UNION ALL SELECT 'session authorization' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
+" SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"
+"  WHERE substring(rolname,1,%d)='%s'"

-#define Query_for_list_of_grant_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
-"   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"\
-" UNION ALL SELECT 'PUBLIC'"\
-" UNION ALL SELECT 'CURRENT_ROLE'"\
-" UNION ALL SELECT 'CURRENT_USER'"\
-" UNION ALL SELECT 'SESSION_USER'"
+/* add these to Query_for_list_of_roles in GRANT contexts */
+#define Keywords_for_list_of_grant_roles \
+"PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_index_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
 " WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"

 #define Query_for_unique_index_of_table \
 Query_for_index_of_table \
 "       and i.indisunique"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table_not_validated \
-"SELECT pg_catalog.quote_ident(conname) "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)" \
+Query_for_constraint_of_table \
 "       and not con.convalidated"

 #define Query_for_all_table_constraints \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
-" WHERE c.conrelid <> 0 "
+" WHERE c.conrelid <> 0 "\
+"       and substring(conname,1,%d)='%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_type \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(t.typname)='%s'"\
+" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
+"       and t.typname='%s'"\
 "       and pg_catalog.pg_type_is_visible(t.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_constraint \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE pg_catalog.quote_ident(conname)='%s')"
+"         WHERE conname='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_rule_of_table \
-"SELECT pg_catalog.quote_ident(rulename) "\
+"SELECT rulename "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_rule \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE pg_catalog.quote_ident(rulename)='%s')"
+"         WHERE rulename='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_trigger_of_table \
-"SELECT pg_catalog.quote_ident(tgname) "\
+"SELECT tgname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"\
 "       and not tgisinternal"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_trigger \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE pg_catalog.quote_ident(tgname)='%s')"
+"         WHERE tgname='%s')"

 #define Query_for_list_of_ts_configurations \
-"SELECT pg_catalog.quote_ident(cfgname) FROM pg_catalog.pg_ts_config "\
-" WHERE substring(pg_catalog.quote_ident(cfgname),1,%d)='%s'"
+"SELECT cfgname FROM pg_catalog.pg_ts_config "\
+" WHERE substring(cfgname,1,%d)='%s'"

 #define Query_for_list_of_ts_dictionaries \
-"SELECT pg_catalog.quote_ident(dictname) FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(pg_catalog.quote_ident(dictname),1,%d)='%s'"
+"SELECT dictname FROM pg_catalog.pg_ts_dict "\
+" WHERE substring(dictname,1,%d)='%s'"

 #define Query_for_list_of_ts_parsers \
-"SELECT pg_catalog.quote_ident(prsname) FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(pg_catalog.quote_ident(prsname),1,%d)='%s'"
+"SELECT prsname FROM pg_catalog.pg_ts_parser "\
+" WHERE substring(prsname,1,%d)='%s'"

 #define Query_for_list_of_ts_templates \
-"SELECT pg_catalog.quote_ident(tmplname) FROM pg_catalog.pg_ts_template "\
-" WHERE substring(pg_catalog.quote_ident(tmplname),1,%d)='%s'"
+"SELECT tmplname FROM pg_catalog.pg_ts_template "\
+" WHERE substring(tmplname,1,%d)='%s'"

 #define Query_for_list_of_fdws \
-" SELECT pg_catalog.quote_ident(fdwname) "\
+" SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(pg_catalog.quote_ident(fdwname),1,%d)='%s'"
+"  WHERE substring(fdwname,1,%d)='%s'"

 #define Query_for_list_of_servers \
-" SELECT pg_catalog.quote_ident(srvname) "\
+" SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(pg_catalog.quote_ident(srvname),1,%d)='%s'"
+"  WHERE substring(srvname,1,%d)='%s'"

 #define Query_for_list_of_user_mappings \
-" SELECT pg_catalog.quote_ident(usename) "\
+" SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(pg_catalog.quote_ident(usename),1,%d)='%s'"
+"  WHERE substring(usename,1,%d)='%s'"

 #define Query_for_list_of_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s'"
+"  WHERE substring(amname,1,%d)='%s'"

 #define Query_for_list_of_index_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc "\
-" WHERE (%d = pg_catalog.length('%s'))"\
-"   AND (pg_catalog.quote_ident(proname)='%s'"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
+"   AND (proname='%s'"\
 "        OR '\"' || proname || '\"'='%s') "\
 "   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments_with_schema \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
 "   AND n.oid = p.pronamespace "\
-"   AND (pg_catalog.quote_ident(proname)='%s' "\
+"   AND (proname='%s' "\
 "        OR '\"' || proname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_extensions \
-" SELECT pg_catalog.quote_ident(extname) "\
+" SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(pg_catalog.quote_ident(extname),1,%d)='%s'"
+"  WHERE substring(extname,1,%d)='%s'"

 #define Query_for_list_of_available_extensions \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s' AND installed_version IS NULL"
+"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions \
-" SELECT pg_catalog.quote_ident(version) "\
+" SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring(version,1,%d)='%s'"\
+"    AND name='%s'"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions_with_TO \
-" SELECT 'TO ' || pg_catalog.quote_ident(version) "\
+" SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring('TO ' || version,1,%d)='%s'"\
+"    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_event_triggers \
-" SELECT pg_catalog.quote_ident(evtname) "\
+" SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(pg_catalog.quote_ident(evtname),1,%d)='%s'"
+"  WHERE substring(evtname,1,%d)='%s'"

 #define Query_for_list_of_tablesample_methods \
-" SELECT pg_catalog.quote_ident(proname) "\
+" SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(pg_catalog.quote_ident(proname),1,%d)='%s'"
+"        substring(proname,1,%d)='%s'"

 #define Query_for_list_of_policies \
-" SELECT pg_catalog.quote_ident(polname) "\
+" SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(pg_catalog.quote_ident(polname),1,%d)='%s'"
+"  WHERE substring(polname,1,%d)='%s'"

 #define Query_for_list_of_tables_for_policy \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE pg_catalog.quote_ident(polname)='%s')"
+"         WHERE polname='%s')"

 #define Query_for_enum \
 " SELECT name FROM ( "\
-"   SELECT pg_catalog.quote_ident(pg_catalog.unnest(enumvals)) AS name "\
+"   SELECT pg_catalog.unnest(enumvals) AS name "\
 "     FROM pg_catalog.pg_settings "\
 "    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
-"    UNION ALL " \
-"   SELECT 'DEFAULT' ) ss "\
+"    ) ss "\
 "  WHERE pg_catalog.substring(name,1,%%d)='%%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_partition_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
 " WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"\
 "       and c2.relispartition = 'true'"

 #define Query_for_list_of_cursors \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1043,18 +1112,18 @@ Query_for_index_of_table \

 static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(pubname) "
+        " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pg_catalog.quote_ident(pubname),1,%d)='%s'"
+        "  WHERE substring(pubname,1,%d)='%s'"
     },
     {0, NULL}
 };

 static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(s.subname) "
+        " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(pg_catalog.quote_ident(s.subname),1,%d)='%s' "
+        "  WHERE substring(s.subname,1,%d)='%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1069,9 +1138,11 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
 typedef struct
 {
     const char *name;
+    /* Provide at most one of these three types of query: */
     const char *query;            /* simple query, or NULL */
     const VersionedQuery *vquery;    /* versioned query, or NULL */
     const SchemaQuery *squery;    /* schema query, or NULL */
+    const char *const *keywords;    /* keywords to be offered as well */
     const bits32 flags;            /* visibility flags, see below */
 } pgsql_thing_t;

@@ -1080,8 +1151,14 @@ typedef struct
 #define THING_NO_ALTER        (1 << 2)    /* should not show up after ALTER */
 #define THING_NO_SHOW        (THING_NO_CREATE | THING_NO_DROP | THING_NO_ALTER)

+/* When we have DROP USER etc, also offer MAPPING FOR */
+static const char *const Keywords_for_user_thing[] = {
+    "MAPPING FOR",
+    NULL
+};
+
 static const pgsql_thing_t words_after_create[] = {
-    {"ACCESS METHOD", NULL, NULL, NULL, THING_NO_ALTER},
+    {"ACCESS METHOD", NULL, NULL, NULL, NULL, THING_NO_ALTER},
     {"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates},
     {"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
                                  * skip it */
@@ -1091,11 +1168,11 @@ static const pgsql_thing_t words_after_create[] = {
      * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
      * to be used only by pg_dump.
      */
-    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT pg_catalog.quote_ident(conname) FROM pg_catalog.pg_conversion WHERE
substring(pg_catalog.quote_ident(conname),1,%d)='%s'"},
+    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, NULL, THING_NO_SHOW},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
     {"DATABASE", Query_for_list_of_databases},
-    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
-    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, THING_NO_SHOW},
+    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, NULL, THING_NO_SHOW},
     {"DOMAIN", NULL, NULL, &Query_for_list_of_domains},
     {"EVENT TRIGGER", NULL, NULL, NULL},
     {"EXTENSION", Query_for_list_of_extensions},
@@ -1105,41 +1182,41 @@ static const pgsql_thing_t words_after_create[] = {
     {"GROUP", Query_for_list_of_roles},
     {"INDEX", NULL, NULL, &Query_for_list_of_indexes},
     {"LANGUAGE", Query_for_list_of_languages},
-    {"LARGE OBJECT", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews},
     {"OPERATOR", NULL, NULL, NULL}, /* Querying for this is probably not such
                                      * a good idea. */
-    {"OR REPLACE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
-    {"OWNED", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
-    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, THING_NO_SHOW},
+    {"OR REPLACE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
+    {"OWNED", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
+    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, NULL, THING_NO_SHOW},
     {"POLICY", NULL, NULL, NULL},
     {"PROCEDURE", NULL, NULL, Query_for_list_of_procedures},
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
-    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, THING_NO_CREATE},
-    {"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE
substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
+    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
     {"STATISTICS", NULL, NULL, &Query_for_list_of_statistics},
     {"SUBSCRIPTION", NULL, Query_for_list_of_subscriptions},
-    {"SYSTEM", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"SYSTEM", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"TABLE", NULL, NULL, &Query_for_list_of_tables},
     {"TABLESPACE", Query_for_list_of_tablespaces},
-    {"TEMP", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TEMP TABLE
-                                                                 * ... */
-    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, THING_NO_SHOW},
-    {"TEMPORARY", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
-                                                                         * TABLE ... */
+    {"TEMP", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMP TABLE
+                                                                         * ... */
+    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, NULL, THING_NO_SHOW},
+    {"TEMPORARY", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
+                                                                             * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
-    {"TRANSFORM", NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT pg_catalog.quote_ident(tgname) FROM pg_catalog.pg_trigger WHERE
substring(pg_catalog.quote_ident(tgname),1,%d)='%s'AND NOT tgisinternal"}, 
+    {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
-    {"UNIQUE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNIQUE
-                                                                     * INDEX ... */
-    {"UNLOGGED", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNLOGGED
-                                                                     * TABLE ... */
-    {"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"},
+    {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
+                                                                         * INDEX ... */
+    {"UNLOGGED", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNLOGGED
+                                                                             * TABLE ... */
+    {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
     {"USER MAPPING FOR", NULL, NULL, NULL},
     {"VIEW", NULL, NULL, &Query_for_list_of_views},
     {NULL}                        /* end of list */
@@ -1200,6 +1277,8 @@ static char *complete_from_schema_query(const char *text, int state);
 static char *complete_from_versioned_schema_query(const char *text, int state);
 static char *_complete_from_query(const char *simple_query,
                                   const SchemaQuery *schema_query,
+                                  const char *const *keywords,
+                                  bool verbatim,
                                   const char *text, int state);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
@@ -1212,6 +1291,12 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static void parse_identifier(const char *ident,
+                             char **schemaname, char **objectname,
+                             bool *schemaquoted, bool *objectquoted);
+static char *requote_identifier(const char *schemaname, const char *objectname,
+                                bool quote_schema, bool quote_object);
+static bool identifier_needs_quotes(const char *ident);
 static PGresult *exec_query(const char *query);

 static char **get_previous_words(int point, char **buffer, int *nwords);
@@ -1650,8 +1735,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE */
     else if (Matches("ALTER", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ALL IN TABLESPACE");

     /* ALTER something */
     else if (Matches("ALTER"))
@@ -1699,11 +1784,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
         COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     /* ALTER PUBLICATION <name> SET ( */
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny) && TailMatches("SET", "("))
         COMPLETE_WITH("publish", "publish_via_partition_root");
@@ -1782,14 +1867,14 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions_with_TO);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
         completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* ALTER FOREIGN */
@@ -1811,8 +1896,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER INDEX */
     else if (Matches("ALTER", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ALL IN TABLESPACE");
     /* ALTER INDEX <name> */
     else if (Matches("ALTER", "INDEX", MatchAny))
         COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET",
@@ -1821,7 +1906,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH"))
         COMPLETE_WITH("PARTITION");
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     /* ALTER INDEX <name> ALTER */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER"))
         COMPLETE_WITH("COLUMN");
@@ -1878,8 +1963,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER MATERIALIZED VIEW */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "ALL IN TABLESPACE");

     /* ALTER USER,ROLE <name> */
     else if (Matches("ALTER", "USER|ROLE", MatchAny) &&
@@ -1973,7 +2058,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "SYSTEM"))
         COMPLETE_WITH("SET", "RESET");
     else if (Matches("ALTER", "SYSTEM", "SET|RESET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_alter_system_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_alter_system_set_vars,
+                                 "all");
     else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
         COMPLETE_WITH("TO");
     /* ALTER VIEW <name> */
@@ -1982,9 +2068,9 @@ psql_completion(const char *text, int start, int end)
                       "SET SCHEMA");
     /* ALTER VIEW xxx RENAME */
     else if (Matches("ALTER", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER VIEW xxx ALTER [ COLUMN ] yyy */
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER", MatchAny) ||
              Matches("ALTER", "VIEW", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2003,9 +2089,9 @@ psql_completion(const char *text, int start, int end)
                       "RESET (", "SET");
     /* ALTER MATERIALIZED VIEW xxx RENAME */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER MATERIALIZED VIEW xxx RENAME yyy */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME", MatchAnyExcept("TO")))
         COMPLETE_WITH("TO");
@@ -2032,7 +2118,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RENAME TO", "TO", "USING (", "WITH CHECK (");
     /* ALTER POLICY <name> ON <table> TO <role> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* ALTER POLICY <name> ON <table> USING ( */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2073,7 +2160,7 @@ psql_completion(const char *text, int start, int end)
      * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
      */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
@@ -2100,7 +2187,7 @@ psql_completion(const char *text, int start, int end)
     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")))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     /* ALTER TABLE xxx ADD CONSTRAINT yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
         COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY");
@@ -2164,13 +2251,13 @@ psql_completion(const char *text, int start, int end)
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx NO */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO"))
         COMPLETE_WITH("FORCE ROW LEVEL SECURITY", "INHERIT");
     /* ALTER TABLE xxx NO INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO", "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx DISABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE"))
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
@@ -2187,13 +2274,13 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE xxx ALTER */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT");

     /* ALTER TABLE xxx RENAME */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT", "TO");
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

     /* ALTER TABLE xxx RENAME yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME", MatchAnyExcept("CONSTRAINT|TO")))
@@ -2208,7 +2295,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("COLUMN", "CONSTRAINT");
     /* If we have ALTER TABLE <sth> DROP COLUMN, provide list of columns */
     else if (Matches("ALTER", "TABLE", MatchAny, "DROP", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
@@ -2298,7 +2385,7 @@ psql_completion(const char *text, int start, int end)
      * tables.
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("ATTACH", "PARTITION", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2360,7 +2447,7 @@ psql_completion(const char *text, int start, int end)
      * of attributes
      */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER|DROP|RENAME", "ATTRIBUTE"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TYPE ALTER ATTRIBUTE <foo> */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER", "ATTRIBUTE", MatchAny))
         COMPLETE_WITH("TYPE");
@@ -2385,8 +2472,8 @@ psql_completion(const char *text, int start, int end)
  * ANALYZE [ VERBOSE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables,
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_analyzables,
+                                        "VERBOSE");
     else if (HeadMatches("ANALYZE", "(*") &&
              !HeadMatches("ANALYZE", "(*)"))
     {
@@ -2402,9 +2489,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("ANALYZE") && TailMatches("("))
         /* "ANALYZE (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables);

 /* BEGIN */
     else if (Matches("BEGIN"))
@@ -2425,19 +2512,20 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("CHAIN");
 /* CALL */
     else if (Matches("CALL"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
     else if (Matches("CALL", MatchAny))
         COMPLETE_WITH("(");
 /* CLOSE */
     else if (Matches("CLOSE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL");
 /* CLUSTER */
     else if (Matches("CLUSTER"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, "UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables,
+                                        "VERBOSE");
     else if (Matches("CLUSTER", "VERBOSE") ||
              Matches("CLUSTER", "(*)"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables);
     /* If we have CLUSTER <sth>, then add "USING" */
     else if (Matches("CLUSTER", MatchAnyExcept("VERBOSE|ON|(|(*)")))
         COMPLETE_WITH("USING");
@@ -2487,19 +2575,19 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_constraint
-                            " UNION SELECT 'DOMAIN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                 "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (Matches("COMMENT", "ON", "EVENT", "TRIGGER"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);
     else if (Matches("COMMENT", "ON", "FOREIGN"))
         COMPLETE_WITH("DATA WRAPPER", "TABLE");
     else if (Matches("COMMENT", "ON", "FOREIGN", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (Matches("COMMENT", "ON", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("COMMENT", "ON", "POLICY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_policies);
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny))
@@ -2529,7 +2617,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH", "TEMPLATE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_templates);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -2557,8 +2645,7 @@ psql_completion(const char *text, int start, int end)
      * backslash command).
      */
     else if (Matches("COPY|\\copy"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION ALL SELECT '('");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables, "(");
     /* Complete COPY ( with legal query commands */
     else if (Matches("COPY|\\copy", "("))
         COMPLETE_WITH("SELECT", "TABLE", "VALUES", "INSERT INTO", "UPDATE", "DELETE FROM", "WITH");
@@ -2616,7 +2703,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "COLLATION", MatchAny))
         COMPLETE_WITH("(", "FROM");
     else if (Matches("CREATE", "COLLATION", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);
     else if (HeadMatches("CREATE", "COLLATION", MatchAny, "(*"))
     {
         if (TailMatches("(|*,"))
@@ -2642,12 +2729,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "DOMAIN", MatchAny))
         COMPLETE_WITH("AS");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS", MatchAny))
         COMPLETE_WITH("COLLATE", "DEFAULT", "CONSTRAINT",
                       "NOT NULL", "NULL", "CHECK (");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "COLLATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);

     /* CREATE EXTENSION */
     /* Complete with available extensions rather than installed ones. */
@@ -2660,7 +2747,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* CREATE FOREIGN */
@@ -2685,9 +2772,8 @@ psql_completion(const char *text, int start, int end)
      * existing indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'"
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON", "CONCURRENTLY");

     /*
      * Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of relations
@@ -2695,15 +2781,15 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("INDEX|CONCURRENTLY", MatchAny, "ON") ||
              TailMatches("INDEX|CONCURRENTLY", "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);

     /*
      * Complete CREATE|UNIQUE INDEX CONCURRENTLY with "ON" and existing
      * indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON");
     /* Complete CREATE|UNIQUE INDEX [CONCURRENTLY] <sth> with "ON" */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", MatchAny) ||
              TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY", MatchAny))
@@ -2718,10 +2804,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("(", "USING");
     else if (TailMatches("INDEX", MatchAny, "ON", MatchAny, "(") ||
              TailMatches("INDEX|CONCURRENTLY", "ON", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* same if you put in USING */
     else if (TailMatches("ON", MatchAny, "USING", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev4_wd, "");
+        COMPLETE_WITH_ATTR(prev4_wd);
     /* Complete USING with an index method */
     else if (TailMatches("INDEX", MatchAny, MatchAny, "ON", MatchAny, "USING") ||
              TailMatches("INDEX", MatchAny, "ON", MatchAny, "USING") ||
@@ -2742,7 +2828,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     /* Complete "CREATE POLICY <name> ON <table>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Complete "CREATE POLICY <name> ON <table> AS|FOR|TO|USING|WITH CHECK" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("AS", "FOR", "TO", "USING (", "WITH CHECK (");
@@ -2770,7 +2856,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO", "USING (", "WITH CHECK (");
     /* Complete "CREATE POLICY <name> ON <table> TO <role>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "CREATE POLICY <name> ON <table> USING (" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2808,7 +2895,8 @@ psql_completion(const char *text, int start, int end)
      * <role>"
      */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "AS", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);

     /*
      * Complete "CREATE POLICY <name> ON <table> AS PERMISSIVE|RESTRICTIVE
@@ -2831,18 +2919,18 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete "CREATE PUBLICATION <name> FOR ALL TABLES IN SCHEMA <schema>,
      * ..."
      */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA' ");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) &&
(!ends_with(prev_wd,','))) 
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> [...] WITH" */
@@ -2871,7 +2959,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO");
     /* Complete "AS ON <sth> TO" with a table name */
     else if (TailMatches("AS", "ON", "SELECT|UPDATE|INSERT|DELETE", "TO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE SEQUENCE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     else if (TailMatches("CREATE", "SEQUENCE", MatchAny) ||
@@ -2898,7 +2986,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (HeadMatches("CREATE", "STATISTICS", MatchAny) &&
              TailMatches("FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
@@ -2912,7 +3000,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RANGE (", "LIST (", "HASH (");
     /* If we have xxx PARTITION OF, provide a list of partitioned tables */
     else if (TailMatches("PARTITION", "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("PARTITION", "OF", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2923,7 +3011,7 @@ psql_completion(const char *text, int start, int end)
     /* Complete CREATE TABLE <name> OF with list of composite types */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "OF") ||
              TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
     /* Complete CREATE TABLE name (...) with supported options */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)") ||
              TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)"))
@@ -2962,7 +3050,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "TRANSFORM", "FOR") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny) ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
@@ -3030,7 +3118,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete CREATE [ OR REPLACE ] TRIGGER ... INSTEAD OF event ON with a
@@ -3038,7 +3126,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("ON", MatchAny))
@@ -3143,7 +3231,7 @@ psql_completion(const char *text, int start, int end)
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* CREATE ROLE,USER,GROUP <name> */
     else if (Matches("CREATE", "ROLE|GROUP|USER", MatchAny) &&
@@ -3179,7 +3267,7 @@ psql_completion(const char *text, int start, int end)
     else if (HeadMatches("CREATE", "TYPE", MatchAny, "AS", "("))
     {
         if (TailMatches("(|*,", MatchAny))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("(|*,", MatchAny, MatchAnyExcept("*)")))
             COMPLETE_WITH("COLLATE", ",", ")");
     }
@@ -3263,12 +3351,12 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("CREATE", "EVENT", "TRIGGER") &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* DEALLOCATE */
     else if (Matches("DEALLOCATE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_prepared_statements,
+                                 "ALL");

 /* DECLARE */

@@ -3316,7 +3404,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FROM");
     /* Complete DELETE FROM with a list of tables */
     else if (TailMatches("DELETE", "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete DELETE FROM <table> */
     else if (TailMatches("DELETE", "FROM", MatchAny))
         COMPLETE_WITH("USING", "WHERE");
@@ -3358,10 +3446,10 @@ psql_completion(const char *text, int start, int end)

     /* DROP INDEX */
     else if (Matches("DROP", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("DROP", "INDEX", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY", MatchAny))
@@ -3371,7 +3459,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("DROP", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("DROP", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");

@@ -3440,7 +3528,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "TRANSFORM"))
         COMPLETE_WITH("FOR");
     else if (Matches("DROP", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -3494,28 +3582,28 @@ psql_completion(const char *text, int start, int end)
      * NEXT, PRIOR, FIRST, LAST, FROM, IN, and a list of cursors
      */
     else if (Matches("FETCH|MOVE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ABSOLUTE'"
-                            " UNION SELECT 'BACKWARD'"
-                            " UNION SELECT 'FORWARD'"
-                            " UNION SELECT 'RELATIVE'"
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'NEXT'"
-                            " UNION SELECT 'PRIOR'"
-                            " UNION SELECT 'FIRST'"
-                            " UNION SELECT 'LAST'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ABSOLUTE",
+                                 "BACKWARD",
+                                 "FORWARD",
+                                 "RELATIVE",
+                                 "ALL",
+                                 "NEXT",
+                                 "PRIOR",
+                                 "FIRST",
+                                 "LAST",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH BACKWARD or FORWARD with one of ALL, FROM, IN, and a
      * list of cursors
      */
     else if (Matches("FETCH|MOVE", "BACKWARD|FORWARD"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH <direction> with "FROM" or "IN". These are equivalent,
@@ -3525,9 +3613,9 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("FETCH|MOVE", "ABSOLUTE|BACKWARD|FORWARD|RELATIVE",
                      MatchAnyExcept("FROM|IN")) ||
              Matches("FETCH|MOVE", "ALL|NEXT|PRIOR|FIRST|LAST"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "FROM",
+                                 "IN");
     /* Complete FETCH <direction> "FROM" or "IN" with a list of cursors */
     else if (HeadMatches("FETCH|MOVE") &&
              TailMatches("FROM|IN"))
@@ -3546,7 +3634,7 @@ psql_completion(const char *text, int start, int end)
 /* FOREIGN TABLE */
     else if (TailMatches("FOREIGN", "TABLE") &&
              !TailMatches("CREATE", MatchAny, MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);

 /* FOREIGN SERVER */
     else if (TailMatches("FOREIGN", "SERVER"))
@@ -3568,20 +3656,20 @@ psql_completion(const char *text, int start, int end)
                           "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
                           "EXECUTE", "USAGE", "ALL");
         else
-            COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                                " UNION SELECT 'SELECT'"
-                                " UNION SELECT 'INSERT'"
-                                " UNION SELECT 'UPDATE'"
-                                " UNION SELECT 'DELETE'"
-                                " UNION SELECT 'TRUNCATE'"
-                                " UNION SELECT 'REFERENCES'"
-                                " UNION SELECT 'TRIGGER'"
-                                " UNION SELECT 'CREATE'"
-                                " UNION SELECT 'CONNECT'"
-                                " UNION SELECT 'TEMPORARY'"
-                                " UNION SELECT 'EXECUTE'"
-                                " UNION SELECT 'USAGE'"
-                                " UNION SELECT 'ALL'");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                     "SELECT",
+                                     "INSERT",
+                                     "UPDATE",
+                                     "DELETE",
+                                     "TRUNCATE",
+                                     "REFERENCES",
+                                     "TRIGGER",
+                                     "CREATE",
+                                     "CONNECT",
+                                     "TEMPORARY",
+                                     "EXECUTE",
+                                     "USAGE",
+                                     "ALL");
     }

     /*
@@ -3601,9 +3689,6 @@ psql_completion(const char *text, int start, int end)
     /*
      * Complete GRANT/REVOKE <sth> ON with a list of appropriate relations.
      *
-     * Keywords like DATABASE, FUNCTION, LANGUAGE and SCHEMA added to query
-     * result via UNION; seems to work intuitively.
-     *
      * Note: GRANT/REVOKE can get quite complex; tab-completion as implemented
      * here will only work if the privilege list contains exactly one
      * privilege.
@@ -3617,26 +3702,26 @@ psql_completion(const char *text, int start, int end)
         if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
             COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
         else
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables,
-                                       " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
-                                       " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
-                                       " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
-                                       " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
-                                       " UNION SELECT 'ALL TABLES IN SCHEMA'"
-                                       " UNION SELECT 'DATABASE'"
-                                       " UNION SELECT 'DOMAIN'"
-                                       " UNION SELECT 'FOREIGN DATA WRAPPER'"
-                                       " UNION SELECT 'FOREIGN SERVER'"
-                                       " UNION SELECT 'FUNCTION'"
-                                       " UNION SELECT 'LANGUAGE'"
-                                       " UNION SELECT 'LARGE OBJECT'"
-                                       " UNION SELECT 'PROCEDURE'"
-                                       " UNION SELECT 'ROUTINE'"
-                                       " UNION SELECT 'SCHEMA'"
-                                       " UNION SELECT 'SEQUENCE'"
-                                       " UNION SELECT 'TABLE'"
-                                       " UNION SELECT 'TABLESPACE'"
-                                       " UNION SELECT 'TYPE'");
+            COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
+                                            "ALL FUNCTIONS IN SCHEMA",
+                                            "ALL PROCEDURES IN SCHEMA",
+                                            "ALL ROUTINES IN SCHEMA",
+                                            "ALL SEQUENCES IN SCHEMA",
+                                            "ALL TABLES IN SCHEMA",
+                                            "DATABASE",
+                                            "DOMAIN",
+                                            "FOREIGN DATA WRAPPER",
+                                            "FOREIGN SERVER",
+                                            "FUNCTION",
+                                            "LANGUAGE",
+                                            "LARGE OBJECT",
+                                            "PROCEDURE",
+                                            "ROUTINE",
+                                            "SCHEMA",
+                                            "SEQUENCE",
+                                            "TABLE",
+                                            "TABLESPACE",
+                                            "TYPE");
     }
     else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
         COMPLETE_WITH("FUNCTIONS IN SCHEMA",
@@ -3658,25 +3743,25 @@ psql_completion(const char *text, int start, int end)
         if (TailMatches("DATABASE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_databases);
         else if (TailMatches("DOMAIN"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
         else if (TailMatches("FUNCTION"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
         else if (TailMatches("LANGUAGE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_languages);
         else if (TailMatches("PROCEDURE"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
         else if (TailMatches("ROUTINE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
         else if (TailMatches("SCHEMA"))
             COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
         else if (TailMatches("SEQUENCE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
         else if (TailMatches("TABLE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
         else if (TailMatches("TABLESPACE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
         else if (TailMatches("TYPE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
             COMPLETE_WITH("TO");
         else
@@ -3689,10 +3774,12 @@ psql_completion(const char *text, int start, int end)
      */
     else if ((HeadMatches("GRANT") && TailMatches("TO")) ||
              (HeadMatches("REVOKE") && TailMatches("FROM")))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "ALTER DEFAULT PRIVILEGES ... GRANT/REVOKE ... TO/FROM */
     else if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES") && TailMatches("TO|FROM"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "GRANT/REVOKE ... ON * *" with TO/FROM */
     else if (HeadMatches("GRANT") && TailMatches("ON", MatchAny, MatchAny))
         COMPLETE_WITH("TO");
@@ -3753,10 +3840,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("INTO");
     /* Complete INSERT INTO with table names */
     else if (TailMatches("INSERT", "INTO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete "INSERT INTO <table> (" with attribute names */
     else if (TailMatches("INSERT", "INTO", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

     /*
      * Complete INSERT INTO <table> with "(" or "VALUES" or "SELECT" or
@@ -3788,14 +3875,13 @@ psql_completion(const char *text, int start, int end)
 /* LOCK */
     /* Complete LOCK [TABLE] [ONLY] with a list of tables */
     else if (Matches("LOCK"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "TABLE", "ONLY");
     else if (Matches("LOCK", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ONLY");
     else if (Matches("LOCK", "TABLE", "ONLY") || Matches("LOCK", "ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* For the following, handle the case of a single table only for now */

     /* Complete LOCK [TABLE] [ONLY] <table> with IN or NOWAIT */
@@ -3831,7 +3917,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s'"); 
+        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -3845,7 +3931,7 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatches("FROM", MatchAny, "ORDER"))
         COMPLETE_WITH("BY");
     else if (TailMatches("FROM", MatchAny, "ORDER", "BY"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

 /* PREPARE xx AS */
     else if (Matches("PREPARE", MatchAny, "AS"))
@@ -3874,10 +3960,10 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("REFRESH", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "CONCURRENTLY");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("WITH");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY", MatchAny))
@@ -3897,26 +3983,26 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TABLE", "INDEX", "SYSTEM", "SCHEMA", "DATABASE");
     else if (Matches("REINDEX", "TABLE") ||
              Matches("REINDEX", "(*)", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexables,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "INDEX") ||
              Matches("REINDEX", "(*)", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "SCHEMA") ||
              Matches("REINDEX", "(*)", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "SYSTEM|DATABASE") ||
              Matches("REINDEX", "(*)", "SYSTEM|DATABASE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_databases
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_databases,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "TABLE", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "TABLE", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);
     else if (Matches("REINDEX", "INDEX", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("REINDEX", "SCHEMA", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "SCHEMA", "CONCURRENTLY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
@@ -3960,9 +4046,17 @@ psql_completion(const char *text, int start, int end)
 /* SET, RESET, SHOW */
     /* Complete with a variable name */
     else if (TailMatches("SET|RESET") && !TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_set_vars,
+                                 "constraints",
+                                 "transaction",
+                                 "session",
+                                 "role",
+                                 "tablespace",
+                                 "all");
     else if (Matches("SHOW"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_show_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_show_vars,
+                                 "session authorization",
+                                 "all");
     /* Complete "SET TRANSACTION" */
     else if (Matches("SET", "TRANSACTION"))
         COMPLETE_WITH("SNAPSHOT", "ISOLATION LEVEL", "READ", "DEFERRABLE", "NOT DEFERRABLE");
@@ -3997,7 +4091,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ONLY", "WRITE");
     /* SET CONSTRAINTS */
     else if (Matches("SET", "CONSTRAINTS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema, "UNION SELECT 'ALL'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_constraints_with_schema,
+                                        "ALL");
     /* Complete SET CONSTRAINTS <foo> with DEFERRED|IMMEDIATE */
     else if (Matches("SET", "CONSTRAINTS", MatchAny))
         COMPLETE_WITH("DEFERRED", "IMMEDIATE");
@@ -4009,7 +4104,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("AUTHORIZATION", "CHARACTERISTICS AS TRANSACTION");
     /* Complete SET SESSION AUTHORIZATION with username */
     else if (Matches("SET", "SESSION", "AUTHORIZATION"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles " UNION SELECT 'DEFAULT'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "DEFAULT");
     /* Complete RESET SESSION with AUTHORIZATION */
     else if (Matches("RESET", "SESSION"))
         COMPLETE_WITH("AUTHORIZATION");
@@ -4039,10 +4135,10 @@ psql_completion(const char *text, int start, int end)
                           "US", "European", "NonEuropean",
                           "DEFAULT");
         else if (TailMatches("search_path", "TO|="))
-            COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                                " AND nspname not like 'pg\\_toast%%' "
-                                " AND nspname not like 'pg\\_temp%%' "
-                                " UNION SELECT 'DEFAULT' ");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                     " AND nspname not like 'pg\\_toast%%' "
+                                     " AND nspname not like 'pg\\_temp%%' ",
+                                     "DEFAULT");
         else
         {
             /* generic, type based, GUC support */
@@ -4061,7 +4157,7 @@ psql_completion(const char *text, int start, int end)

                     snprintf(querybuf, sizeof(querybuf),
                              Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY(querybuf);
+                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4080,7 +4176,7 @@ psql_completion(const char *text, int start, int end)

 /* TABLE, but not TABLE embedded in other commands */
     else if (Matches("TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* TABLESAMPLE */
     else if (TailMatches("TABLESAMPLE"))
@@ -4090,14 +4186,13 @@ psql_completion(const char *text, int start, int end)

 /* TRUNCATE */
     else if (Matches("TRUNCATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "TABLE", "ONLY");
     else if (Matches("TRUNCATE", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "ONLY");
     else if (HeadMatches("TRUNCATE") && TailMatches("ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables);
     else if (Matches("TRUNCATE", MatchAny) ||
              Matches("TRUNCATE", "TABLE|ONLY", MatchAny) ||
              Matches("TRUNCATE", "TABLE", "ONLY", MatchAny))
@@ -4107,18 +4202,20 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s' UNION SELECT '*'"); 
+        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
+                                 " WHERE substring(channel,1,%d)='%s'",
+                                 "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
     else if (TailMatches("UPDATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete UPDATE <table> with "SET" */
     else if (TailMatches("UPDATE", MatchAny))
         COMPLETE_WITH("SET");
     /* Complete UPDATE <table> SET with list of attributes */
     else if (TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* UPDATE <table> SET <attr> = */
     else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
         COMPLETE_WITH("=");
@@ -4127,11 +4224,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER|CREATE|DROP", "USER", "MAPPING"))
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "USER", "MAPPING", "FOR"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                            " UNION SELECT 'CURRENT_ROLE'"
-                            " UNION SELECT 'CURRENT_USER'"
-                            " UNION SELECT 'PUBLIC'"
-                            " UNION SELECT 'USER'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "CURRENT_ROLE",
+                                 "CURRENT_USER",
+                                 "PUBLIC",
+                                 "USER");
     else if (Matches("ALTER|DROP", "USER", "MAPPING", "FOR"))
         COMPLETE_WITH_QUERY(Query_for_list_of_user_mappings);
     else if (Matches("CREATE|ALTER|DROP", "USER", "MAPPING", "FOR", MatchAny))
@@ -4144,26 +4241,26 @@ psql_completion(const char *text, int start, int end)
  * VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FULL'"
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FULL",
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FULL"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FREEZE") ||
              Matches("VACUUM", "FULL", "FREEZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'VERBOSE'"
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "VERBOSE",
+                                        "ANALYZE");
     else if (Matches("VACUUM", "VERBOSE") ||
              Matches("VACUUM", "FULL|FREEZE", "VERBOSE") ||
              Matches("VACUUM", "FULL", "FREEZE", "VERBOSE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "ANALYZE");
     else if (HeadMatches("VACUUM", "(*") &&
              !HeadMatches("VACUUM", "(*)"))
     {
@@ -4184,9 +4281,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("VACUUM") && TailMatches("("))
         /* "VACUUM (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables);

 /* WITH [RECURSIVE] */

@@ -4200,16 +4297,16 @@ psql_completion(const char *text, int start, int end)
 /* WHERE */
     /* Simple case of the word before the where being the table name */
     else if (TailMatches(MatchAny, "WHERE"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

 /* ... FROM ... */
 /* TODO: also include SRF ? */
     else if (TailMatches("FROM") && !Matches("COPY|\\copy", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* ... JOIN ... */
     else if (TailMatches("JOIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* Backslash commands */
 /* TODO:  \dc \dd \dl */
@@ -4226,19 +4323,19 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     }
     else if (TailMatchesCS("\\da*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates);
     else if (TailMatchesCS("\\dAc*", MatchAny) ||
              TailMatchesCS("\\dAf*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dAo*", MatchAny) ||
              TailMatchesCS("\\dAp*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families);
     else if (TailMatchesCS("\\dA*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
     else if (TailMatchesCS("\\db*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
     else if (TailMatchesCS("\\dD*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (TailMatchesCS("\\des*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_servers);
     else if (TailMatchesCS("\\deu*"))
@@ -4246,9 +4343,9 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatchesCS("\\dew*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_fdws);
     else if (TailMatchesCS("\\df*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
     else if (HeadMatchesCS("\\df*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);

     else if (TailMatchesCS("\\dFd*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_dictionaries);
@@ -4261,51 +4358,51 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_configurations);

     else if (TailMatchesCS("\\di*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (TailMatchesCS("\\dL*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     else if (TailMatchesCS("\\dn*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
     /* no support for completing operators, but we can complete types: */
     else if (HeadMatchesCS("\\do*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dp") || TailMatchesCS("\\z"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
     else if (TailMatchesCS("\\dPi*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes);
     else if (TailMatchesCS("\\dPt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     else if (TailMatchesCS("\\dP*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations);
     else if (TailMatchesCS("\\ds*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
     else if (TailMatchesCS("\\dt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     else if (TailMatchesCS("\\dT*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\du*") || TailMatchesCS("\\dg*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     else if (TailMatchesCS("\\dv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\dx*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
     else if (TailMatchesCS("\\dX*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics);
     else if (TailMatchesCS("\\dm*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (TailMatchesCS("\\dE*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (TailMatchesCS("\\dy*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);

     /* must be at end of \d alternatives: */
     else if (TailMatchesCS("\\d*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations);

     else if (TailMatchesCS("\\ef"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\ev"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
         COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
@@ -4407,9 +4504,9 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_CS("default", "verbose", "terse", "sqlstate");
     }
     else if (TailMatchesCS("\\sf*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\sv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|"
                            "\\ir|\\include_relative|\\o|\\out|"
                            "\\s|\\w|\\write|\\lo_import"))
@@ -4426,19 +4523,21 @@ psql_completion(const char *text, int start, int end)
      */
     else
     {
-        int            i;
+        const pgsql_thing_t *wac;

-        for (i = 0; words_after_create[i].name; i++)
+        for (wac = words_after_create; wac->name != NULL; wac++)
         {
-            if (pg_strcasecmp(prev_wd, words_after_create[i].name) == 0)
+            if (pg_strcasecmp(prev_wd, wac->name) == 0)
             {
-                if (words_after_create[i].query)
-                    COMPLETE_WITH_QUERY(words_after_create[i].query);
-                else if (words_after_create[i].vquery)
-                    COMPLETE_WITH_VERSIONED_QUERY(words_after_create[i].vquery);
-                else if (words_after_create[i].squery)
-                    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(words_after_create[i].squery,
-                                                         NULL);
+                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;
             }
         }
@@ -4551,7 +4650,8 @@ static char *
 complete_from_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, NULL, text, state);
+    return _complete_from_query(completion_charp, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
@@ -4566,22 +4666,22 @@ complete_from_versioned_query(const char *text, int state)
     if (vquery->query == NULL)
         return NULL;

-    return _complete_from_query(vquery->query, NULL, text, state);
+    return _complete_from_query(vquery->query, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
 complete_from_schema_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, completion_squery,
-                                text, state);
+    return _complete_from_query(NULL, completion_squery, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
 complete_from_versioned_schema_query(const char *text, int state)
 {
     const SchemaQuery *squery = completion_squery;
-    const VersionedQuery *vquery = completion_vquery;

     /* Find appropriate array element */
     while (pset.sversion < squery->min_server_version)
@@ -4590,17 +4690,8 @@ complete_from_versioned_schema_query(const char *text, int state)
     if (squery->catname == NULL)
         return NULL;

-    /* Likewise for the add-on text, if any */
-    if (vquery)
-    {
-        while (pset.sversion < vquery->min_server_version)
-            vquery++;
-        if (vquery->query == NULL)
-            return NULL;
-    }
-
-    return _complete_from_query(vquery ? vquery->query : NULL,
-                                squery, text, state);
+    return _complete_from_query(NULL, squery, completion_charpp,
+                                completion_verbatim, text, state);
 }


@@ -4611,35 +4702,54 @@ complete_from_versioned_schema_query(const char *text, int state)
  *
  * The query can be one of two kinds:
  *
- * 1. A simple query which must contain a %d and a %s, which will be replaced
- * by the string length of the text and the text itself. The query may also
- * have up to four more %s in it; the first two such will be replaced by the
- * value of completion_info_charp, the next two by the value of
- * completion_info_charp2.
+ * 1. A simple query, which must contain a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
+ * The query may also have up to four more %s in it; the first two such will
+ * be replaced by the value of completion_info_charp, the next two by the
+ * value of completion_info_charp2.  (These strings will be escaped to
+ * become SQL literals, so what is actually in the query should be '%s'.)
+ * Simple queries should return a single column of matches.  If "verbatim"
+ * is true, the matches are returned as-is; otherwise, they are taken to
+ * be SQL identifiers and quoted if necessary.
  *
  * 2. A schema query used for completion of both schema and relation names.
- * These are more complex and must contain in the following order:
- * %d %s %d %s %d %s %s %d %s
- * where %d is the string length of the text and %s the text itself.
+ * This is represented by a SchemaQuery object; see that typedef for details.
+ *
+ * See top of file for examples of both kinds of query.
  *
- * If both simple_query and schema_query are non-NULL, then we construct
- * a schema query and append the (uninterpreted) string simple_query to it.
+ * In addition to the query itself, we accept a null-terminated array of
+ * literal keywords, which will be returned if they match the input-so-far
+ * (case insensitively).  (These are in addition to keywords specified
+ * within the schema_query, if any.)
  *
- * It is assumed that strings should be escaped to become SQL literals
- * (that is, what is in the query is actually ... '%s' ...)
+ * If "verbatim" is true, then we use the given text as-is to match the
+ * query results; otherwise we parse it as a possibly-qualified identifier,
+ * and reconstruct suitable quoting afterward.
  *
- * See top of file for examples of both kinds of query.
+ * "text" and "state" are supplied by Readline.  "text" is the word we are
+ * trying to complete.  "state" is zero on first call, nonzero later.
  *
- * "text" and "state" are supplied by readline.
+ * readline will call this repeatedly with the same text and varying
+ * state.  On each call, we are supposed to return a malloc'd string
+ * that is a candidate completion.  Return NULL when done.
  */
 static char *
 _complete_from_query(const char *simple_query,
                      const SchemaQuery *schema_query,
+                     const char *const *keywords,
+                     bool verbatim,
                      const char *text, int state)
 {
     static int    list_index,
-                byte_length;
+                num_schema_only,
+                num_other;
     static PGresult *result = NULL;
+    static bool non_empty_object;
+    static bool schemaquoted;
+    static bool objectquoted;

     /*
      * If this is the first time for this completion, we fetch a list of our
@@ -4648,31 +4758,53 @@ _complete_from_query(const char *simple_query,
     if (state == 0)
     {
         PQExpBufferData query_buffer;
-        char       *e_text;
+        char       *schemaname;
+        char       *objectname;
+        int            object_length = 0;
+        char       *e_schemaname;
+        char       *e_objectname;
         char       *e_info_charp;
         char       *e_info_charp2;
-        const char *pstr = text;
-        int            char_length = 0;

+        /* Reset static state, ensuring no memory leaks */
         list_index = 0;
-        byte_length = strlen(text);
+        num_schema_only = 0;
+        num_other = 0;
+        PQclear(result);
+        result = NULL;
+
+        /* Parse text, splitting into schema and object name if needed */
+        if (verbatim)
+        {
+            objectname = pg_strdup(text);
+            schemaname = NULL;
+        }
+        else
+        {
+            parse_identifier(text,
+                             &schemaname, &objectname,
+                             &schemaquoted, &objectquoted);
+        }
+
+        /* Remember whether the user has typed anything in the object part */
+        non_empty_object = (*objectname != '\0');

         /*
          * Count length as number of characters (not bytes), for passing to
          * substring
          */
-        while (*pstr)
-        {
-            char_length++;
-            pstr += PQmblenBounded(pstr, pset.encoding);
-        }
-
-        /* Free any prior result */
-        PQclear(result);
-        result = NULL;
+        for (const char *p = objectname;
+             *p;
+             p += PQmblenBounded(p, pset.encoding))
+            object_length++;

         /* Set up suitably-escaped copies of textual inputs */
-        e_text = escape_string(text);
+        if (schemaname)
+            e_schemaname = escape_string(schemaname);
+        else
+            e_schemaname = NULL;
+
+        e_objectname = escape_string(objectname);

         if (completion_info_charp)
             e_info_charp = escape_string(completion_info_charp);
@@ -4688,14 +4820,17 @@ _complete_from_query(const char *simple_query,

         if (schema_query)
         {
-            /* schema_query gives us the pieces to assemble */
-            const char *qualresult = schema_query->qualresult;
-
-            if (qualresult == NULL)
-                qualresult = schema_query->result;
+            Assert(simple_query == NULL);

+            /*
+             * We issue different queries depending on whether the input is
+             * already qualified or not.  schema_query gives us the pieces to
+             * assemble.
+             */
+            if (schemaname == NULL)
+            {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s FROM %s WHERE ",
+            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
                               schema_query->result,
                               schema_query->catname);
             if (schema_query->selcondition)
@@ -4703,7 +4838,7 @@ _complete_from_query(const char *simple_query,
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
-                              char_length, e_text);
+                              object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);

@@ -4715,72 +4850,59 @@ _complete_from_query(const char *simple_query,
              */
             if (strcmp(schema_query->catname,
                        "pg_catalog.pg_class c") == 0 &&
-                strncmp(text, "pg_", 3) != 0)
+                strncmp(objectname, "pg_", 3) != 0)
             {
                 appendPQExpBufferStr(&query_buffer,
                                      " AND c.relnamespace <> (SELECT oid FROM"
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

+            /* Add in schema names matching the input-so-far */
+            appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
+                              "SELECT NULL::pg_catalog.text, n.nspname "
+                              "FROM pg_catalog.pg_namespace n "
+                              "WHERE substring(n.nspname,1,%d)='%s'",
+                              object_length, e_objectname);
+
             /*
-             * Add in matching schema names, but only if there is more than
-             * one potential match among schema names.
+             * Likewise, suppress system schemas unless the input-so-far
+             * begins with "pg_".
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' "
-                              "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d)='%s'",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) > 1",
-                              char_length, e_text);
+            if (strncmp(objectname, "pg_", 3) != 0)
+                appendPQExpBufferStr(&query_buffer,
+                                     " AND n.nspname NOT LIKE 'pg\\_%'");

             /*
-             * Add in matching qualified names, but only if there is exactly
-             * one schema matching the input-so-far.
+             * Since we're matching these schema names to the object name,
+             * handle their quoting using the object name's quoting state.
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' || %s "
+            schemaquoted = objectquoted;
+            }
+            else
+            {
+            /* Input is qualified, so produce only qualified names */
+            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
                               "FROM %s, pg_catalog.pg_namespace n "
                               "WHERE %s = n.oid AND ",
-                              qualresult,
+                              schema_query->result,
                               schema_query->catname,
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(pg_catalog.quote_ident(n.nspname) || '.' || %s,1,%d)='%s'",
-                              qualresult,
-                              char_length, e_text);
-
-            /*
-             * This condition exploits the single-matching-schema rule to
-             * speed up the query
-             */
-            appendPQExpBuffer(&query_buffer,
-                              " AND substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(n.nspname))+1)",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1",
-                              char_length, e_text);
-
-            /* If an addon query was provided, use it */
-            if (simple_query)
-                appendPQExpBuffer(&query_buffer, "\n%s", simple_query);
+            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                              schema_query->result,
+                              object_length, e_objectname);
+            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                              e_schemaname);
+            }
         }
         else
         {
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              char_length, e_text,
+                              object_length, e_objectname,
                               e_info_charp, e_info_charp,
                               e_info_charp2, e_info_charp2);
         }
@@ -4789,28 +4911,120 @@ _complete_from_query(const char *simple_query,
         appendPQExpBuffer(&query_buffer, "\nLIMIT %d",
                           completion_max_records);

+        /* Finally, we can issue the query */
         result = exec_query(query_buffer.data);

+        /* Clean up */
         termPQExpBuffer(&query_buffer);
-        free(e_text);
+        if (e_schemaname)
+            free(e_schemaname);
+        free(e_objectname);
         if (e_info_charp)
             free(e_info_charp);
         if (e_info_charp2)
             free(e_info_charp2);
     }

-    /* Find something that matches */
+    /* Return the next result, if any, but not if the query failed */
     if (result && PQresultStatus(result) == PGRES_TUPLES_OK)
     {
-        const char *item;
+        int            nskip;

-        while (list_index < PQntuples(result) &&
-               (item = PQgetvalue(result, list_index++, 0)))
-            if (pg_strncasecmp(text, item, byte_length) == 0)
+        while (list_index < PQntuples(result))
+        {
+            const char *item = NULL;
+            const char *nsp = NULL;
+
+            if (!PQgetisnull(result, list_index, 0))
+                item = PQgetvalue(result, list_index, 0);
+            if (PQnfields(result) > 1 &&
+                !PQgetisnull(result, list_index, 1))
+                nsp = PQgetvalue(result, list_index, 1);
+            list_index++;
+
+            /* In verbatim mode, we return all the items as-is */
+            if (verbatim)
                 return pg_strdup(item);
+
+            /*
+             * In normal mode, a name requiring quoting will be returned only
+             * if the input was empty or quoted.  Otherwise the user might see
+             * completion inserting a quote she didn't type, which is
+             * surprising.  This restriction also dodges some odd behaviors of
+             * some versions of readline/libedit.
+             */
+            if (non_empty_object)
+            {
+                if (item && !objectquoted && identifier_needs_quotes(item))
+                    continue;
+                if (nsp && !schemaquoted && identifier_needs_quotes(nsp))
+                    continue;
+            }
+
+            /* Count schema-only results for hack below */
+            if (item == NULL && nsp != NULL)
+                num_schema_only++;
+            else
+                num_other++;
+
+            return requote_identifier(nsp, item, schemaquoted, objectquoted);
+        }
+
+        /*
+         * When the query result is exhausted, check for hard-wired keywords.
+         * These will only be returned if they match the input-so-far,
+         * ignoring case.
+         */
+        nskip = list_index - PQntuples(result);
+        if (schema_query && schema_query->keywords)
+        {
+            const char *const *itemp = schema_query->keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                {
+                    num_other++;
+                    return pg_strdup(item);
+                }
+            }
+        }
+        if (keywords)
+        {
+            const char *const *itemp = keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                {
+                    num_other++;
+                    return pg_strdup(item);
+                }
+            }
+        }
     }

-    /* If nothing matches, free the db structure and return null */
+    /*
+     * Hack: if we returned only bare schema names, don't let Readline add a
+     * space afterwards.  Otherwise the schema will stop being part of the
+     * completion subject text, which is not what we want.
+     */
+#ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
+    if (num_schema_only > 0 && num_other == 0)
+        rl_completion_append_character = '\0';
+#endif
+
+    /* No more matches, so free the result structure and return null */
     PQclear(result);
     result = NULL;
     return NULL;
@@ -5144,6 +5358,228 @@ escape_string(const char *text)
 }


+/*
+ * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
+ *
+ * This involves splitting off the schema name if present, de-quoting,
+ * and downcasing any unquoted text.  We are a bit laxer than the backend
+ * in that we allow just portions of a name to be quoted --- that's because
+ * psql metacommands have traditionally behaved that way.
+ *
+ * Outputs are a malloc'd schema name (NULL if none), malloc'd object name,
+ * and booleans telling whether any part of the schema and object name was
+ * double-quoted.
+ */
+static void
+parse_identifier(const char *ident,
+                 char **schemaname, char **objectname,
+                 bool *schemaquoted, bool *objectquoted)
+{
+    size_t        buflen = strlen(ident) + 1;
+    bool        enc_is_single_byte = (pg_encoding_max_length(pset.encoding) == 1);
+    char       *sname;
+    char       *oname;
+    char       *optr;
+    bool        inquotes;
+
+    /* Initialize, making a certainly-large-enough output buffer */
+    sname = NULL;
+    oname = pg_malloc(buflen);
+    *schemaquoted = *objectquoted = false;
+    /* Scan */
+    optr = oname;
+    inquotes = false;
+    while (*ident)
+    {
+        unsigned char ch = (unsigned char) *ident++;
+
+        if (ch == '"')
+        {
+            if (inquotes && *ident == '"')
+            {
+                /* two quote marks within a quoted identifier = emit quote */
+                *optr++ = '"';
+                ident++;
+            }
+            else
+            {
+                inquotes = !inquotes;
+                *objectquoted = true;
+            }
+        }
+        else if (ch == '.' && !inquotes)
+        {
+            /* Found a schema name, transfer it to sname / *schemaquoted */
+            *optr = '\0';
+            free(sname);        /* drop any catalog name */
+            sname = oname;
+            oname = pg_malloc(buflen);
+            optr = oname;
+            *schemaquoted = *objectquoted;
+            *objectquoted = false;
+        }
+        else if (!enc_is_single_byte && IS_HIGHBIT_SET(ch))
+        {
+            /*
+             * Transfer multibyte characters without further processing.  They
+             * wouldn't be affected by our downcasing rule anyway, and this
+             * avoids possibly doing the wrong thing in unsafe client
+             * encodings.
+             */
+            int            chlen = PQmblenBounded(ident - 1, pset.encoding);
+
+            *optr++ = (char) ch;
+            while (--chlen > 0)
+                *optr++ = *ident++;
+        }
+        else
+        {
+            if (!inquotes)
+            {
+                /*
+                 * This downcasing transformation should match the backend's
+                 * downcase_identifier() as best we can.  We do not know the
+                 * backend's locale, though, so it's necessarily approximate.
+                 * We assume that psql is operating in the same locale and
+                 * encoding as the backend.
+                 */
+                if (ch >= 'A' && ch <= 'Z')
+                    ch += 'a' - 'A';
+                else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch))
+                    ch = tolower(ch);
+            }
+            *optr++ = (char) ch;
+        }
+    }
+
+    *optr = '\0';
+    *schemaname = sname;
+    *objectname = oname;
+}
+
+
+/*
+ * requote_identifier - Reconstruct a possibly-schema-qualified SQL identifier.
+ *
+ * Build a malloc'd string containing the identifier, with quoting applied
+ * as necessary.  This is more or less the inverse of parse_identifier;
+ * in particular, if an input component was quoted, we'll quote the output
+ * even when that isn't strictly required.
+ *
+ * Unlike parse_identifier, we handle the case where a schema and no
+ * object name is provided, producing just "schema.".
+ */
+static char *
+requote_identifier(const char *schemaname, const char *objectname,
+                   bool quote_schema, bool quote_object)
+{
+    char       *result;
+    size_t        buflen = 1;        /* count the trailing \0 */
+    char       *ptr;
+
+    /*
+     * We could use PQescapeIdentifier for some of this, but not all, and it
+     * adds more notational cruft than it seems worth.
+     */
+    if (schemaname)
+    {
+        buflen += strlen(schemaname) + 1;    /* +1 for the dot */
+        if (!quote_schema)
+            quote_schema = identifier_needs_quotes(schemaname);
+        if (quote_schema)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = schemaname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    if (objectname)
+    {
+        buflen += strlen(objectname);
+        if (!quote_object)
+            quote_object = identifier_needs_quotes(objectname);
+        if (quote_object)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = objectname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    result = pg_malloc(buflen);
+    ptr = result;
+    if (schemaname)
+    {
+        if (quote_schema)
+            *ptr++ = '"';
+        for (const char *p = schemaname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_schema)
+            *ptr++ = '"';
+        *ptr++ = '.';
+    }
+    if (objectname)
+    {
+        if (quote_object)
+            *ptr++ = '"';
+        for (const char *p = objectname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_object)
+            *ptr++ = '"';
+    }
+    *ptr = '\0';
+    return result;
+}
+
+
+/*
+ * Detect whether an identifier must be double-quoted.
+ *
+ * Note we'll quote anything that's not ASCII; the backend's quote_ident()
+ * does the same.  Perhaps this could be relaxed in future.
+ */
+static bool
+identifier_needs_quotes(const char *ident)
+{
+    int            kwnum;
+
+    /* Check syntax. */
+    if (!((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'))
+        return true;
+    if (strspn(ident, "abcdefghijklmnopqrstuvwxyz0123456789_") != strlen(ident))
+        return true;
+
+    /*
+     * Check for keyword.  We quote keywords except for unreserved ones.
+     *
+     * It is possible that our keyword list doesn't quite agree with the
+     * server's, but this should be close enough for tab-completion purposes.
+     *
+     * Note: ScanKeywordLookup() does case-insensitive comparison, but that's
+     * fine, since we already know we have all-lower-case.
+     */
+    kwnum = ScanKeywordLookup(ident, &ScanKeywords);
+
+    if (kwnum >= 0 && ScanKeywordCategories[kwnum] != UNRESERVED_KEYWORD)
+        return true;
+
+    return false;
+}
+
+
 /*
  * Execute a query and report any errors. This should be the preferred way of
  * talking to the database in this file.
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 16999dd9a2..5658ec3759 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -110,6 +110,12 @@ typedef struct VersionedQuery
  * time.  So we put the components of each query into this struct and
  * assemble them with the common boilerplate in _complete_from_query().
  *
+ * We also use this struct to define queries that use completion_info_object,
+ * which is some object related to the one(s) we want to get the names of
+ * (for example, the table we want the indexes of).  In that usage the
+ * objects we're completing might not have a schema of their own, but the
+ * related object almost always does (passed in completion_info_schema).
+ *
  * As with VersionedQuery, we can use an array of these if the query details
  * must vary across versions.
  */
@@ -123,8 +129,9 @@ typedef struct SchemaQuery
     int            min_server_version;

     /*
-     * Name of catalog or catalogs to be queried, with alias, eg.
-     * "pg_catalog.pg_class c".  Note that "pg_namespace n" will be added.
+     * Name of catalog or catalogs to be queried, with alias(es), eg.
+     * "pg_catalog.pg_class c".  Note that "pg_namespace n" and/or
+     * "pg_namespace ni" will be added automatically when needed.
      */
     const char *catname;

@@ -140,12 +147,15 @@ typedef struct SchemaQuery
     /*
      * Visibility condition --- which rows are visible without schema
      * qualification?  For example, "pg_catalog.pg_table_is_visible(c.oid)".
+     * NULL if not needed.
      */
     const char *viscondition;

     /*
-     * Namespace --- name of field to join to pg_namespace.oid. For example,
-     * "c.relnamespace".
+     * Namespace --- name of field to join to pg_namespace.oid when there is
+     * schema qualification.  For example, "c.relnamespace".  NULL if we don't
+     * want to join to pg_namespace (then any schema part in the input word
+     * will be ignored).
      */
     const char *namespace;

@@ -154,12 +164,43 @@ typedef struct SchemaQuery
      */
     const char *result;

+    /*
+     * In some cases, it's difficult to keep the query from returning the same
+     * object multiple times.  Specify use_distinct to filter out duplicates.
+     */
+    bool        use_distinct;
+
     /*
      * Additional literal strings (usually keywords) to be offered along with
      * the query results.  Provide a NULL-terminated array of constant
      * strings, or NULL if none.
      */
     const char *const *keywords;
+
+    /*
+     * If this query uses completion_info_object/completion_info_schema,
+     * populate the remaining fields, else leave them NULL.  When using this
+     * capability, catname must include the catalog that defines the
+     * completion_info_object, and selcondition must include the join
+     * condition that connects it to the result's catalog.
+     *
+     * infoname is the field that should be equated to completion_info_object,
+     * for example "ci.relname".
+     */
+    const char *infoname;
+
+    /*
+     * Visibility condition to use when completion_info_schema is not set. For
+     * example, "pg_catalog.pg_table_is_visible(ci.oid)".  NULL if not needed.
+     */
+    const char *infoviscondition;
+
+    /*
+     * Name of field to join to pg_namespace.oid when completion_info_schema
+     * is set.  For example, "ci.relnamespace".  NULL if we don't want to
+     * consider completion_info_schema.
+     */
+    const char *infonamespace;
 } SchemaQuery;


@@ -176,10 +217,10 @@ static int    completion_max_records;
 static char completion_last_char;    /* last char of input word */
 static const char *completion_charp;    /* to pass a string */
 static const char *const *completion_charpp;    /* to pass a list of strings */
-static const char *completion_info_charp;    /* to pass a second string */
-static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
+static char *completion_info_object;    /* name of a related object */
+static char *completion_info_schema;    /* schema name of a related object */
 static bool completion_case_sensitive;    /* completion is case sensitive */
 static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */
@@ -257,6 +298,14 @@ do { \
     COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
 } while (0)

+#define COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(query) \
+do { \
+    completion_squery = &(query); \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
+} while (0)
+
 #define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
     COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)

@@ -312,29 +361,11 @@ do { \

 #define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_table; \
-\
-    _completion_schema = strtokx(relation, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_table = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                false, false, pset.encoding); \
-    if (_completion_table == NULL) \
-    { \
-        completion_charp = Query_for_list_of_attributes; \
-        completion_info_charp = relation; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_attributes_with_schema; \
-        completion_info_charp = _completion_table; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(relation); \
+    completion_squery = &(Query_for_list_of_attributes); \
     completion_charpp = list; \
     completion_verbatim = false; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
@@ -351,65 +382,24 @@ do { \
  */
 #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); \
-    use_quotes = (text[0] == '\'' || \
-                  start == 0 || rl_line_buffer[start - 1] != '\''); \
-    if (_completion_type == NULL) \
-    { \
-        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; \
-    } \
+    setup_completion_info(type); \
+    if (text[0] == '\'' || \
+        start == 0 || rl_line_buffer[start - 1] != '\'') \
+        completion_squery = &(Query_for_list_of_enum_values_quoted); \
     else \
-    { \
-        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; \
-    } \
+        completion_squery = &(Query_for_list_of_enum_values_unquoted); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_FUNCTION_ARG(function) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_function; \
-\
-    _completion_schema = strtokx(function, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_function = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                   false, false, pset.encoding); \
-    if (_completion_function == NULL) \
-    { \
-        completion_charp = Query_for_list_of_arguments; \
-        completion_info_charp = function; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_arguments_with_schema; \
-        completion_info_charp = _completion_function; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(function); \
+    completion_squery = &(Query_for_list_of_arguments); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 /*
@@ -419,6 +409,51 @@ do { \
  * unnecessary bloat in the completions generated.
  */

+static const SchemaQuery Query_for_constraint_of_table = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_table_not_validated = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid and not con.convalidated",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_type = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_type t",
+    .selcondition = "con.contypid=t.oid",
+    .result = "con.conname",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_unique_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid and i.indisunique",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
 static const SchemaQuery Query_for_list_of_aggregates[] = {
     {
         .min_server_version = 110000,
@@ -437,6 +472,32 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
     }
 };

+static const SchemaQuery Query_for_list_of_arguments = {
+    .catname = "pg_catalog.pg_proc p",
+    .result = "pg_catalog.oidvectortypes(p.proargtypes)||')'",
+    .infoname = "p.proname",
+    .infoviscondition = "pg_catalog.pg_function_is_visible(p.oid)",
+    .infonamespace = "p.pronamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attributes = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attname",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attribute_numbers = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attnum::pg_catalog.text",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
 static const char *const Keywords_for_list_of_datatypes[] = {
     "bigint",
     "boolean",
@@ -497,6 +558,24 @@ static const SchemaQuery Query_for_list_of_domains = {
     .result = "t.typname",
 };

+static const SchemaQuery Query_for_list_of_enum_values_quoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "pg_catalog.quote_literal(enumlabel)",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_enum_values_unquoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "e.enumlabel",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
 /* Note: this intentionally accepts aggregates as well as plain functions */
 static const SchemaQuery Query_for_list_of_functions[] = {
     {
@@ -571,6 +650,48 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .result = "c.relname",
 };

+static const SchemaQuery Query_for_list_of_tables_for_constraint = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_constraint con",
+    .selcondition = "c.oid=con.conrelid and c.relkind IN ("
+    CppAsString2(RELKIND_RELATION) ", "
+    CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "con.conname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_policy = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_policy p",
+    .selcondition = "c.oid=p.polrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "p.polname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_rule = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_rewrite r",
+    .selcondition = "c.oid=r.ev_class",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "r.rulename",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_trigger = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_trigger t",
+    .selcondition = "c.oid=t.tgrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "t.tgname",
+};
+
 static const SchemaQuery Query_for_list_of_views = {
     .catname = "pg_catalog.pg_class c",
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
@@ -719,7 +840,6 @@ static const SchemaQuery Query_for_list_of_clusterables = {
 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .catname = "pg_catalog.pg_constraint c",
     .selcondition = "c.conrelid <> 0",
-    .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
     .result = "c.conname",
 };
@@ -739,96 +859,56 @@ static const SchemaQuery Query_for_list_of_collations = {
     .result = "c.collname",
 };

+static const SchemaQuery Query_for_partition_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
+    .selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
+    .viscondition = "pg_catalog.pg_table_is_visible(c2.oid)",
+    .namespace = "c2.relnamespace",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_rule_of_table = {
+    .catname = "pg_catalog.pg_rewrite r, pg_catalog.pg_class c1",
+    .selcondition = "r.ev_class=c1.oid",
+    .result = "r.rulename",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_trigger_of_table = {
+    .catname = "pg_catalog.pg_trigger t, pg_catalog.pg_class c1",
+    .selcondition = "t.tgrelid=c1.oid and not t.tgisinternal",
+    .result = "t.tgname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+

 /*
  * Queries to get lists of names of various kinds of things, possibly
- * restricted to names matching a partially entered name.  In these queries,
- * the first %s will be replaced by the text entered so far (suitably escaped
- * to become a SQL literal string).  %d will be replaced by the length of the
- * string (in unescaped form).  A second and third %s, if present, will be
- * replaced by a suitably-escaped version of the string provided in
- * completion_info_charp.  A fourth and fifth %s are similarly replaced by
- * completion_info_charp2.
+ * restricted to names matching a partially entered name.  Don't use
+ * this method where the user might wish to enter a schema-qualified
+ * name; make a SchemaQuery instead.
+ *
+ * In these queries, there must be a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
  *
- * Beware that the allowed sequences of %s and %d are determined by
- * _complete_from_query().
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  NOTE: using completion_info_object
+ * that way is usually the wrong thing, and using completion_info_schema
+ * that way is always the wrong thing.  Make a SchemaQuery instead.
  */

-#define Query_for_list_of_attributes \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attribute_numbers \
-"SELECT attnum "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attributes_with_schema \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
-" WHERE c.oid = a.attrelid "\
-"   AND n.oid = c.relnamespace "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
-#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 "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND pg_catalog.pg_type_is_visible(t.oid)"
-
-#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 (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 "\
-"   AND n.oid = t.typnamespace "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (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 (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_template_databases \
 "SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
@@ -887,81 +967,12 @@ static const SchemaQuery Query_for_list_of_collations = {
 #define Keywords_for_list_of_grant_roles \
 "PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-#define Query_for_index_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
-" WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"
-
-#define Query_for_unique_index_of_table \
-Query_for_index_of_table \
-"       and i.indisunique"
-
-#define Query_for_constraint_of_table \
-"SELECT conname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_constraint_of_table_not_validated \
-Query_for_constraint_of_table \
-"       and not con.convalidated"
-
 #define Query_for_all_table_constraints \
 "SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "\
 "       and substring(conname,1,%d)='%s'"

-#define Query_for_constraint_of_type \
-"SELECT conname "\
-"  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
-"       and t.typname='%s'"\
-"       and pg_catalog.pg_type_is_visible(t.oid)"
-
-#define Query_for_list_of_tables_for_constraint \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE conname='%s')"
-
-#define Query_for_rule_of_table \
-"SELECT rulename "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_list_of_tables_for_rule \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE rulename='%s')"
-
-#define Query_for_trigger_of_table \
-"SELECT tgname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"\
-"       and not tgisinternal"
-
-#define Query_for_list_of_tables_for_trigger \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE tgname='%s')"
-
 #define Query_for_list_of_ts_configurations \
 "SELECT cfgname FROM pg_catalog.pg_ts_config "\
 " WHERE substring(cfgname,1,%d)='%s'"
@@ -1010,24 +1021,6 @@ Query_for_constraint_of_table \
 "  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-#define Query_for_list_of_arguments \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND (proname='%s'"\
-"        OR '\"' || proname || '\"'='%s') "\
-"   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"
-
-#define Query_for_list_of_arguments_with_schema \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND n.oid = p.pronamespace "\
-"   AND (proname='%s' "\
-"        OR '\"' || proname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_extensions \
 " SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
@@ -1074,30 +1067,18 @@ Query_for_constraint_of_table \
 "   FROM pg_catalog.pg_policy "\
 "  WHERE substring(polname,1,%d)='%s'"

-#define Query_for_list_of_tables_for_policy \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE polname='%s')"
-
-#define Query_for_enum \
-" SELECT name FROM ( "\
-"   SELECT pg_catalog.unnest(enumvals) AS name "\
+#define Query_for_values_of_enum_GUC \
+" SELECT val FROM ( "\
+"   SELECT name, pg_catalog.unnest(enumvals) AS val "\
 "     FROM pg_catalog.pg_settings "\
-"    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
 "    ) ss "\
-"  WHERE pg_catalog.substring(name,1,%%d)='%%s'"
+"  WHERE substring(val,1,%d)='%s'"\
+"        and pg_catalog.lower(name)=pg_catalog.lower('%s')"

-#define Query_for_partition_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
-" WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"\
-"       and c2.relispartition = 'true'"
+#define Query_for_list_of_channels \
+" SELECT channel "\
+"   FROM pg_catalog.pg_listening_channels() AS channel "\
+"  WHERE substring(channel,1,%d)='%s'"

 #define Query_for_list_of_cursors \
 " SELECT name "\
@@ -1280,6 +1261,7 @@ static char *_complete_from_query(const char *simple_query,
                                   const char *const *keywords,
                                   bool verbatim,
                                   const char *text, int state);
+static void setup_completion_info(const char *word);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
 static void append_variable_names(char ***varnames, int *nvars,
@@ -1685,8 +1667,10 @@ psql_completion(const char *text, int start, int end)
     /* Clear a few things. */
     completion_charp = NULL;
     completion_charpp = NULL;
-    completion_info_charp = NULL;
-    completion_info_charp2 = NULL;
+    completion_vquery = NULL;
+    completion_squery = NULL;
+    completion_info_object = NULL;
+    completion_info_schema = NULL;

     /*
      * Scan the input line to extract the words before our current position.
@@ -1866,14 +1850,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER EXTENSION <name> UPDATE */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
-        completion_info_charp = prev3_wd;
+        setup_completion_info(prev3_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -1913,8 +1897,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER INDEX <name> ALTER COLUMN */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_attribute_numbers);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(Query_for_list_of_attribute_numbers);
     }
     /* ALTER INDEX <name> ALTER COLUMN <colnum> */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2024,8 +2008,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER DOMAIN <sth> DROP|RENAME|VALIDATE CONSTRAINT */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "DROP|RENAME|VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_type);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_type);
     }
     /* ALTER DOMAIN <sth> RENAME */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "RENAME"))
@@ -2110,8 +2094,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER POLICY <name> ON <table> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     /* ALTER POLICY <name> ON <table> - show options */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny))
@@ -2134,8 +2118,8 @@ psql_completion(const char *text, int start, int end)
     /* If we have ALTER RULE <name> ON, then add the correct tablename */
     else if (Matches("ALTER", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }

     /* ALTER RULE <name> ON <name> */
@@ -2150,18 +2134,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");

-    else if (Matches("ALTER", "TRIGGER", MatchAny, MatchAny))
+    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }

-    /*
-     * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
-     */
-    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-
     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("RENAME TO", "DEPENDS ON EXTENSION",
@@ -2200,28 +2178,28 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE xxx ADD PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev6_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev6_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev8_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev8_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev7_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev7_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ENABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE"))
@@ -2231,23 +2209,23 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "RULE"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "TRIGGER"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
@@ -2263,13 +2241,13 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }

     /* ALTER TABLE xxx ALTER */
@@ -2299,14 +2277,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table);
     }
     /* ALTER TABLE <sth> VALIDATE CONSTRAINT <non-validated constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table_not_validated);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table_not_validated);
     }
     /* ALTER TABLE ALTER [COLUMN] <foo> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny) ||
@@ -2338,8 +2316,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("ALTER", "TABLE", MatchAny, "CLUSTER", "ON"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     /* If we have ALTER TABLE <sth> SET, provide list of attributes and '(' */
     else if (Matches("ALTER", "TABLE", MatchAny, "SET"))
@@ -2370,8 +2348,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_LIST(table_storage_parameters);
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING"))
         COMPLETE_WITH("INDEX");
@@ -2398,8 +2376,8 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_partition_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
         COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
@@ -2536,8 +2514,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CLUSTER", MatchAny, "USING") ||
              Matches("CLUSTER", "VERBOSE|(*)", MatchAny, "USING"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (HeadMatches("CLUSTER", "(*") &&
              !HeadMatches("CLUSTER", "(*)"))
@@ -2574,9 +2552,9 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
-                                 "DOMAIN");
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                        "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
@@ -2594,8 +2572,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("COMMENT", "ON", "PROCEDURAL", "LANGUAGE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
@@ -2603,8 +2581,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH"))
         COMPLETE_WITH("CONFIGURATION", "DICTIONARY", "PARSER", "TEMPLATE");
@@ -2622,15 +2600,15 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("COMMENT", "ON", MatchAny, MatchAnyExcept("IS")) ||
              Matches("COMMENT", "ON", MatchAny, MatchAny, MatchAnyExcept("IS")) ||
@@ -2746,7 +2724,7 @@ psql_completion(const char *text, int start, int end)
     /* CREATE EXTENSION <name> VERSION */
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -3057,7 +3035,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }

@@ -3480,8 +3458,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3507,8 +3485,8 @@ psql_completion(const char *text, int start, int end)
     /* DROP POLICY <name> ON <table> */
     else if (Matches("DROP", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("DROP", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3518,8 +3496,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3533,7 +3511,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
@@ -3917,7 +3895,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");
+        COMPLETE_WITH_QUERY(Query_for_list_of_channels);

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -4153,11 +4131,9 @@ psql_completion(const char *text, int start, int end)
             {
                 if (strcmp(guctype, "enum") == 0)
                 {
-                    char        querybuf[1024];
-
-                    snprintf(querybuf, sizeof(querybuf),
-                             Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
+                    setup_completion_info(prev2_wd);
+                    COMPLETE_WITH_QUERY_PLUS(Query_for_values_of_enum_GUC,
+                                             "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4202,9 +4178,7 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
-                                 " WHERE substring(channel,1,%d)='%s'",
-                                 "*");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_channels, "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
@@ -4405,7 +4379,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_encodings);
     else if (TailMatchesCS("\\h|\\help"))
         COMPLETE_WITH_LIST(sql_commands);
     else if (TailMatchesCS("\\h|\\help", MatchAny))
@@ -4564,6 +4538,12 @@ psql_completion(const char *text, int start, int end)
     free(previous_words);
     free(words_buffer);
     free(text_copy);
+    if (completion_info_object)
+        free(completion_info_object);
+    completion_info_object = NULL;
+    if (completion_info_schema)
+        free(completion_info_schema);
+    completion_info_schema = NULL;

     /* Return our Grand List O' Matches */
     return matches;
@@ -4707,10 +4687,11 @@ complete_from_versioned_schema_query(const char *text, int state)
  * where "OUTPUT" is the same string that the query returns.  The %d and %s
  * will be replaced by the string length of the text and the text itself,
  * causing the results to be limited to those matching the already-typed text.
- * The query may also have up to four more %s in it; the first two such will
- * be replaced by the value of completion_info_charp, the next two by the
- * value of completion_info_charp2.  (These strings will be escaped to
- * become SQL literals, so what is actually in the query should be '%s'.)
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  Those strings should be set up
+ * by calling setup_completion_info().
  * Simple queries should return a single column of matches.  If "verbatim"
  * is true, the matches are returned as-is; otherwise, they are taken to
  * be SQL identifiers and quoted if necessary.
@@ -4763,8 +4744,8 @@ _complete_from_query(const char *simple_query,
         int            object_length = 0;
         char       *e_schemaname;
         char       *e_objectname;
-        char       *e_info_charp;
-        char       *e_info_charp2;
+        char       *e_info_object;
+        char       *e_info_schema;

         /* Reset static state, ensuring no memory leaks */
         list_index = 0;
@@ -4806,15 +4787,15 @@ _complete_from_query(const char *simple_query,

         e_objectname = escape_string(objectname);

-        if (completion_info_charp)
-            e_info_charp = escape_string(completion_info_charp);
+        if (completion_info_object)
+            e_info_object = escape_string(completion_info_object);
         else
-            e_info_charp = NULL;
+            e_info_object = NULL;

-        if (completion_info_charp2)
-            e_info_charp2 = escape_string(completion_info_charp2);
+        if (completion_info_schema)
+            e_info_schema = escape_string(completion_info_schema);
         else
-            e_info_charp2 = NULL;
+            e_info_schema = NULL;

         initPQExpBuffer(&query_buffer);

@@ -4827,20 +4808,44 @@ _complete_from_query(const char *simple_query,
              * already qualified or not.  schema_query gives us the pieces to
              * assemble.
              */
-            if (schemaname == NULL)
+            if (schemaname == NULL || schema_query->namespace == NULL)
             {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer,
+                              "%s, NULL::pg_catalog.text FROM %s",
                               schema_query->result,
                               schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBufferStr(&query_buffer, " WHERE ");
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
                               object_length, e_objectname);
+            if (schema_query->viscondition)
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }

             /*
              * When fetching relation names, suppress system catalogs unless
@@ -4857,7 +4862,12 @@ _complete_from_query(const char *simple_query,
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

-            /* Add in schema names matching the input-so-far */
+            /*
+             * If the target object type can be schema-qualified, add in
+             * schema names matching the input-so-far.
+             */
+            if (schema_query->namespace)
+            {
             appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
                               "SELECT NULL::pg_catalog.text, n.nspname "
                               "FROM pg_catalog.pg_namespace n "
@@ -4878,14 +4888,21 @@ _complete_from_query(const char *simple_query,
              */
             schemaquoted = objectquoted;
             }
+            }
             else
             {
             /* Input is qualified, so produce only qualified names */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n "
-                              "WHERE %s = n.oid AND ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                              "FROM %s, pg_catalog.pg_namespace n",
                               schema_query->result,
-                              schema_query->catname,
+                              schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
@@ -4895,6 +4912,21 @@ _complete_from_query(const char *simple_query,
                               object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
                               e_schemaname);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }
             }
         }
         else
@@ -4903,8 +4935,7 @@ _complete_from_query(const char *simple_query,
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
                               object_length, e_objectname,
-                              e_info_charp, e_info_charp,
-                              e_info_charp2, e_info_charp2);
+                              e_info_object, e_info_schema);
         }

         /* Limit the number of records in the result */
@@ -4919,10 +4950,10 @@ _complete_from_query(const char *simple_query,
         if (e_schemaname)
             free(e_schemaname);
         free(e_objectname);
-        if (e_info_charp)
-            free(e_info_charp);
-        if (e_info_charp2)
-            free(e_info_charp2);
+        if (e_info_object)
+            free(e_info_object);
+        if (e_info_schema)
+            free(e_info_schema);
     }

     /* Return the next result, if any, but not if the query failed */
@@ -5031,6 +5062,23 @@ _complete_from_query(const char *simple_query,
 }


+/*
+ * Set up completion_info_object and completion_info_schema
+ * by parsing the given word.  These variables can then be
+ * used in a query passed to _complete_from_query.
+ */
+static void
+setup_completion_info(const char *word)
+{
+    bool        schemaquoted,
+                objectquoted;
+
+    parse_identifier(word,
+                     &completion_info_schema, &completion_info_object,
+                     &schemaquoted, &objectquoted);
+}
+
+
 /*
  * This function returns in order one of a fixed, NULL pointer terminated list
  * of strings (if matching). This can be used if there are only a fixed number
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5658ec3759..01441f9bf4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4810,123 +4810,125 @@ _complete_from_query(const char *simple_query,
              */
             if (schemaname == NULL || schema_query->namespace == NULL)
             {
-            /* Get unqualified names matching the input-so-far */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer,
-                              "%s, NULL::pg_catalog.text FROM %s",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBufferStr(&query_buffer, " WHERE ");
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
-                              schema_query->result,
-                              object_length, e_objectname);
-            if (schema_query->viscondition)
-            appendPQExpBuffer(&query_buffer, " AND %s",
-                              schema_query->viscondition);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Get unqualified names matching the input-so-far */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer,
+                                  "%s, NULL::pg_catalog.text FROM %s",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
-
-            /*
-             * When fetching relation names, suppress system catalogs unless
-             * the input-so-far begins with "pg_".  This is a compromise
-             * between not offering system catalogs for completion at all, and
-             * having them swamp the result when the input is just "p".
-             */
-            if (strcmp(schema_query->catname,
-                       "pg_catalog.pg_class c") == 0 &&
-                strncmp(objectname, "pg_", 3) != 0)
-            {
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND c.relnamespace <> (SELECT oid FROM"
-                                     " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
-            }
-
-            /*
-             * If the target object type can be schema-qualified, add in
-             * schema names matching the input-so-far.
-             */
-            if (schema_query->namespace)
-            {
-            appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
-                              "SELECT NULL::pg_catalog.text, n.nspname "
-                              "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(n.nspname,1,%d)='%s'",
-                              object_length, e_objectname);
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBufferStr(&query_buffer, " WHERE ");
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                if (schema_query->viscondition)
+                    appendPQExpBuffer(&query_buffer, " AND %s",
+                                      schema_query->viscondition);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
 
-            /*
-             * Likewise, suppress system schemas unless the input-so-far
-             * begins with "pg_".
-             */
-            if (strncmp(objectname, "pg_", 3) != 0)
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND n.nspname NOT LIKE 'pg\\_%'");
+                /*
+                 * When fetching relation names, suppress system catalogs
+                 * unless the input-so-far begins with "pg_".  This is a
+                 * compromise between not offering system catalogs for
+                 * completion at all, and having them swamp the result when
+                 * the input is just "p".
+                 */
+                if (strcmp(schema_query->catname,
+                           "pg_catalog.pg_class c") == 0 &&
+                    strncmp(objectname, "pg_", 3) != 0)
+                {
+                    appendPQExpBufferStr(&query_buffer,
+                                         " AND c.relnamespace <> (SELECT oid FROM"
+                                         " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
+                }
 
-            /*
-             * Since we're matching these schema names to the object name,
-             * handle their quoting using the object name's quoting state.
-             */
-            schemaquoted = objectquoted;
-            }
+                /*
+                 * If the target object type can be schema-qualified, add in
+                 * schema names matching the input-so-far.
+                 */
+                if (schema_query->namespace)
+                {
+                    appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
+                                      "SELECT NULL::pg_catalog.text, n.nspname "
+                                      "FROM pg_catalog.pg_namespace n "
+                                      "WHERE substring(n.nspname,1,%d)='%s'",
+                                      object_length, e_objectname);
+
+                    /*
+                     * Likewise, suppress system schemas unless the
+                     * input-so-far begins with "pg_".
+                     */
+                    if (strncmp(objectname, "pg_", 3) != 0)
+                        appendPQExpBufferStr(&query_buffer,
+                                             " AND n.nspname NOT LIKE 'pg\\_%'");
+
+                    /*
+                     * Since we're matching these schema names to the object
+                     * name, handle their quoting using the object name's
+                     * quoting state.
+                     */
+                    schemaquoted = objectquoted;
+                }
             }
             else
             {
-            /* Input is qualified, so produce only qualified names */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
-                              schema_query->namespace);
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
-                              schema_query->result,
-                              object_length, e_objectname);
-            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
-                              e_schemaname);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Input is qualified, so produce only qualified names */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                                  "FROM %s, pg_catalog.pg_namespace n",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
+                                  schema_query->namespace);
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                                  e_schemaname);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
             }
         }
         else
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index d3d1bd650e..8fda7c7f40 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -40,10 +40,11 @@ $node->start;

 # set up a few database objects
 $node->safe_psql('postgres',
-        "CREATE TABLE tab1 (f1 int, f2 text);\n"
+        "CREATE TABLE tab1 (f1 int primary key, f2 text);\n"
       . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
       . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
-      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz');\n");
+      . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n"
+      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz', 'BLACK');\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.
@@ -176,6 +177,38 @@ check_completion("2\t", qr/246 /,

 clear_query();

+# check handling of quoted names
+check_completion(
+    "select * from \"my\t",
+    qr/select \* from "my\a?tab/,
+    "complete \"my<tab> to \"mytab when there are multiple choices");
+
+check_completion(
+    "\t\t",
+    qr/"mytab123" +"mytab246"/,
+    "offer multiple quoted table choices");
+
+check_completion("2\t", qr/246" /,
+    "finish completion of one of multiple quoted table choices");
+
+clear_query();
+
+# check handling of mixed-case names
+check_completion(
+    "select * from \"mi\t",
+    qr/"mixedName"/,
+    "complete a mixed-case name");
+
+clear_query();
+
+# check case folding
+check_completion(
+    "select * from TAB\t",
+    qr/tab1 /,
+    "automatically fold case");
+
+clear_query();
+
 # check case-sensitive keyword replacement
 # note: various versions of readline/libedit handle backspacing
 # differently, so just check that the replacement comes out correctly
@@ -183,6 +216,48 @@ check_completion("\\DRD\t", qr/drds /, "complete \\DRD<tab> to \\drds");

 clear_query();

+# check completion of a schema-qualified name
+check_completion(
+    "select * from pub\t",
+    qr/public\./,
+    "complete schema when relevant");
+
+check_completion(
+    "tab\t",
+    qr/tab1 /,
+    "complete schema-qualified name");
+
+clear_query();
+
+check_completion(
+    "select * from PUBLIC.t\t",
+    qr/public\.tab1 /,
+    "automatically fold case in schema-qualified name");
+
+clear_query();
+
+# check interpretation of referenced names
+check_completion(
+    "alter table tab1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table");
+
+clear_query();
+
+check_completion(
+    "alter table TAB1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with downcasing");
+
+clear_query();
+
+check_completion(
+    "alter table public.\"tab1\" drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with schema and quoting");
+
+clear_query();
+
 # check filename completion
 check_completion(
     "\\lo_import tmp_check/some\t",
@@ -234,6 +309,14 @@ check_completion(

 clear_line();

+# enum labels are case sensitive, so this should complete BLACK immediately
+check_completion(
+    "ALTER TYPE enum1 RENAME VALUE 'B\t",
+    qr|BLACK|,
+    "enum labels are case sensitive");
+
+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 01441f9bf4..f7efa4b947 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -896,10 +896,9 @@ static const SchemaQuery Query_for_trigger_of_table = {
  * name; make a SchemaQuery instead.
  *
  * In these queries, there must be a restriction clause of the form
- *        substring(OUTPUT,1,%d)='%s'
- * where "OUTPUT" is the same string that the query returns.  The %d and %s
- * will be replaced by the string length of the text and the text itself,
- * causing the results to be limited to those matching the already-typed text.
+ *        output LIKE '%s'
+ * where "output" is the same string that the query returns.  The %s
+ * will be replaced by a LIKE pattern to match the already-typed text.
  *
  * There can be a second '%s', which will be replaced by a suitably-escaped
  * version of the string provided in completion_info_object.  If there is a
@@ -912,56 +911,56 @@ static const SchemaQuery Query_for_trigger_of_table = {
 #define Query_for_list_of_template_databases \
 "SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(d.datname,1,%d)='%s' "\
+" WHERE d.datname LIKE '%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
 "SELECT datname FROM pg_catalog.pg_database "\
-" WHERE substring(datname,1,%d)='%s'"
+" WHERE datname LIKE '%s'"

 #define Query_for_list_of_tablespaces \
 "SELECT spcname FROM pg_catalog.pg_tablespace "\
-" WHERE substring(spcname,1,%d)='%s'"
+" WHERE spcname LIKE '%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
 "   FROM pg_catalog.pg_conversion "\
-"  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"
+"  WHERE pg_catalog.pg_encoding_to_char(conforencoding) LIKE UPPER('%s')"

 #define Query_for_list_of_languages \
 "SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(lanname,1,%d)='%s'"
+"   AND lanname LIKE '%s'"

 #define Query_for_list_of_schemas \
 "SELECT nspname FROM pg_catalog.pg_namespace "\
-" WHERE substring(nspname,1,%d)='%s'"
+" WHERE nspname LIKE '%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context != 'internal' "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context IN ('user', 'superuser') "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_roles \
 " SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(rolname,1,%d)='%s'"
+"  WHERE rolname LIKE '%s'"

 /* add these to Query_for_list_of_roles in GRANT contexts */
 #define Keywords_for_list_of_grant_roles \
@@ -971,119 +970,119 @@ static const SchemaQuery Query_for_trigger_of_table = {
 "SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "\
-"       and substring(conname,1,%d)='%s'"
+"       and conname LIKE '%s'"

 #define Query_for_list_of_ts_configurations \
 "SELECT cfgname FROM pg_catalog.pg_ts_config "\
-" WHERE substring(cfgname,1,%d)='%s'"
+" WHERE cfgname LIKE '%s'"

 #define Query_for_list_of_ts_dictionaries \
 "SELECT dictname FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(dictname,1,%d)='%s'"
+" WHERE dictname LIKE '%s'"

 #define Query_for_list_of_ts_parsers \
 "SELECT prsname FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(prsname,1,%d)='%s'"
+" WHERE prsname LIKE '%s'"

 #define Query_for_list_of_ts_templates \
 "SELECT tmplname FROM pg_catalog.pg_ts_template "\
-" WHERE substring(tmplname,1,%d)='%s'"
+" WHERE tmplname LIKE '%s'"

 #define Query_for_list_of_fdws \
 " SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(fdwname,1,%d)='%s'"
+"  WHERE fdwname LIKE '%s'"

 #define Query_for_list_of_servers \
 " SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(srvname,1,%d)='%s'"
+"  WHERE srvname LIKE '%s'"

 #define Query_for_list_of_user_mappings \
 " SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(usename,1,%d)='%s'"
+"  WHERE usename LIKE '%s'"

 #define Query_for_list_of_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s'"
+"  WHERE amname LIKE '%s'"

 #define Query_for_list_of_index_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s' AND "\
+"  WHERE amname LIKE '%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s' AND "\
+"  WHERE amname LIKE '%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

 #define Query_for_list_of_extensions \
 " SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(extname,1,%d)='%s'"
+"  WHERE extname LIKE '%s'"

 #define Query_for_list_of_available_extensions \
 " SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"
+"  WHERE name LIKE '%s' AND installed_version IS NULL"

 /* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions \
 " SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE substring(version,1,%d)='%s'"\
+"  WHERE version LIKE '%s'"\
 "    AND name='%s'"

 /* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions_with_TO \
 " SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE substring('TO ' || version,1,%d)='%s'"\
+"  WHERE ('TO ' || version) LIKE '%s'"\
 "    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
 " SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(name,1,%d)='%s'"
+"  WHERE name LIKE '%s'"

 #define Query_for_list_of_event_triggers \
 " SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(evtname,1,%d)='%s'"
+"  WHERE evtname LIKE '%s'"

 #define Query_for_list_of_tablesample_methods \
 " SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(proname,1,%d)='%s'"
+"        proname LIKE '%s'"

 #define Query_for_list_of_policies \
 " SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(polname,1,%d)='%s'"
+"  WHERE polname LIKE '%s'"

 #define Query_for_values_of_enum_GUC \
 " SELECT val FROM ( "\
 "   SELECT name, pg_catalog.unnest(enumvals) AS val "\
 "     FROM pg_catalog.pg_settings "\
 "    ) ss "\
-"  WHERE substring(val,1,%d)='%s'"\
+"  WHERE val LIKE '%s'"\
 "        and pg_catalog.lower(name)=pg_catalog.lower('%s')"

 #define Query_for_list_of_channels \
 " SELECT channel "\
 "   FROM pg_catalog.pg_listening_channels() AS channel "\
-"  WHERE substring(channel,1,%d)='%s'"
+"  WHERE channel LIKE '%s'"

 #define Query_for_list_of_cursors \
 " SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(name,1,%d)='%s'"
+"  WHERE name LIKE '%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1095,7 +1094,7 @@ static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
         " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pubname,1,%d)='%s'"
+        "  WHERE pubname LIKE '%s'"
     },
     {0, NULL}
 };
@@ -1104,7 +1103,7 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
         " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(s.subname,1,%d)='%s' "
+        "  WHERE s.subname LIKE '%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1150,7 +1149,7 @@ static const pgsql_thing_t words_after_create[] = {
      * to be used only by pg_dump.
      */
     {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE conname LIKE '%s'"},
     {"DATABASE", Query_for_list_of_databases},
     {"DEFAULT PRIVILEGES", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, NULL, THING_NO_SHOW},
@@ -1175,7 +1174,7 @@ static const pgsql_thing_t words_after_create[] = {
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
     {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
-    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE rulename LIKE '%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
@@ -1191,7 +1190,7 @@ static const pgsql_thing_t words_after_create[] = {
                                                                              * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
     {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
     {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
                                                                          * INDEX ... */
@@ -1273,6 +1272,7 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static char *make_like_pattern(const char *word);
 static void parse_identifier(const char *ident,
                              char **schemaname, char **objectname,
                              bool *schemaquoted, bool *objectquoted);
@@ -1769,9 +1769,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                 " AND nspname != 'pg_catalog' "
-                                 " AND nspname not like 'pg\\_toast%%' "
-                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 " AND nspname NOT LIKE E'pg\\\\_%'",
                                  "CURRENT_SCHEMA");
     /* ALTER PUBLICATION <name> SET ( */
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny) && TailMatches("SET", "("))
@@ -2905,9 +2903,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                 " AND nspname != 'pg_catalog' "
-                                 " AND nspname not like 'pg\\_toast%%' "
-                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 " AND nspname NOT LIKE E'pg\\\\_%'",
                                  "CURRENT_SCHEMA");
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) &&
(!ends_with(prev_wd,','))) 
         COMPLETE_WITH("WITH (");
@@ -4113,10 +4109,13 @@ psql_completion(const char *text, int start, int end)
                           "US", "European", "NonEuropean",
                           "DEFAULT");
         else if (TailMatches("search_path", "TO|="))
+        {
+            /* Here, we want to allow pg_catalog, so use narrower exclusion */
             COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                     " AND nspname not like 'pg\\_toast%%' "
-                                     " AND nspname not like 'pg\\_temp%%' ",
+                                     " AND nspname NOT LIKE E'pg\\\\_toast%%'"
+                                     " AND nspname NOT LIKE E'pg\\\\_temp%%'",
                                      "DEFAULT");
+        }
         else
         {
             /* generic, type based, GUC support */
@@ -4683,10 +4682,9 @@ complete_from_versioned_schema_query(const char *text, int state)
  * The query can be one of two kinds:
  *
  * 1. A simple query, which must contain a restriction clause of the form
- *        substring(OUTPUT,1,%d)='%s'
- * where "OUTPUT" is the same string that the query returns.  The %d and %s
- * will be replaced by the string length of the text and the text itself,
- * causing the results to be limited to those matching the already-typed text.
+ *        output LIKE '%s'
+ * where "output" is the same string that the query returns.  The %s
+ * will be replaced by a LIKE pattern to match the already-typed text.
  * There can be a second '%s', which will be replaced by a suitably-escaped
  * version of the string provided in completion_info_object.  If there is a
  * third '%s', it will be replaced by a suitably-escaped version of the string
@@ -4741,9 +4739,8 @@ _complete_from_query(const char *simple_query,
         PQExpBufferData query_buffer;
         char       *schemaname;
         char       *objectname;
-        int            object_length = 0;
+        char       *e_object_like;
         char       *e_schemaname;
-        char       *e_objectname;
         char       *e_info_object;
         char       *e_info_schema;

@@ -4771,22 +4768,16 @@ _complete_from_query(const char *simple_query,
         non_empty_object = (*objectname != '\0');

         /*
-         * Count length as number of characters (not bytes), for passing to
-         * substring
+         * Convert objectname to a LIKE prefix pattern (e.g. 'foo%'), and set
+         * up suitably-escaped copies of all the strings we need.
          */
-        for (const char *p = objectname;
-             *p;
-             p += PQmblenBounded(p, pset.encoding))
-            object_length++;
+        e_object_like = make_like_pattern(objectname);

-        /* Set up suitably-escaped copies of textual inputs */
         if (schemaname)
             e_schemaname = escape_string(schemaname);
         else
             e_schemaname = NULL;

-        e_objectname = escape_string(objectname);
-
         if (completion_info_object)
             e_info_object = escape_string(completion_info_object);
         else
@@ -4825,9 +4816,9 @@ _complete_from_query(const char *simple_query,
                 if (schema_query->selcondition)
                     appendPQExpBuffer(&query_buffer, "%s AND ",
                                       schema_query->selcondition);
-                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
+                appendPQExpBuffer(&query_buffer, "(%s) LIKE '%s'",
                                   schema_query->result,
-                                  object_length, e_objectname);
+                                  e_object_like);
                 if (schema_query->viscondition)
                     appendPQExpBuffer(&query_buffer, " AND %s",
                                       schema_query->viscondition);
@@ -4872,8 +4863,8 @@ _complete_from_query(const char *simple_query,
                     appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
                                       "SELECT NULL::pg_catalog.text, n.nspname "
                                       "FROM pg_catalog.pg_namespace n "
-                                      "WHERE substring(n.nspname,1,%d)='%s'",
-                                      object_length, e_objectname);
+                                      "WHERE n.nspname LIKE '%s'",
+                                      e_object_like);

                     /*
                      * Likewise, suppress system schemas unless the
@@ -4881,7 +4872,7 @@ _complete_from_query(const char *simple_query,
                      */
                     if (strncmp(objectname, "pg_", 3) != 0)
                         appendPQExpBufferStr(&query_buffer,
-                                             " AND n.nspname NOT LIKE 'pg\\_%'");
+                                             " AND n.nspname NOT LIKE E'pg\\\\_%'");

                     /*
                      * Since we're matching these schema names to the object
@@ -4909,9 +4900,9 @@ _complete_from_query(const char *simple_query,
                 if (schema_query->selcondition)
                     appendPQExpBuffer(&query_buffer, "%s AND ",
                                       schema_query->selcondition);
-                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                appendPQExpBuffer(&query_buffer, "(%s) LIKE '%s' AND ",
                                   schema_query->result,
-                                  object_length, e_objectname);
+                                  e_object_like);
                 appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
                                   e_schemaname);
                 if (schema_query->infoname)
@@ -4936,7 +4927,7 @@ _complete_from_query(const char *simple_query,
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              object_length, e_objectname,
+                              e_object_like,
                               e_info_object, e_info_schema);
         }

@@ -4949,9 +4940,9 @@ _complete_from_query(const char *simple_query,

         /* Clean up */
         termPQExpBuffer(&query_buffer);
+        free(e_object_like);
         if (e_schemaname)
             free(e_schemaname);
-        free(e_objectname);
         if (e_info_object)
             free(e_info_object);
         if (e_info_schema)
@@ -5408,6 +5399,48 @@ escape_string(const char *text)
 }


+/*
+ * make_like_pattern - Convert argument to a LIKE prefix pattern.
+ *
+ * We escape _ and % in the given text by backslashing, append a % to
+ * represent "any subsequent characters", and then pass the string through
+ * escape_string() so it's ready to insert in a query.  The result needs
+ * to be freed.
+ */
+static char *
+make_like_pattern(const char *word)
+{
+    char       *result;
+    char       *buffer = pg_malloc(strlen(word) * 2 + 2);
+    char       *bptr = buffer;
+
+    while (*word)
+    {
+        if (*word == '_' || *word == '%')
+            *bptr++ = '\\';
+        if (IS_HIGHBIT_SET(*word))
+        {
+            /*
+             * Transfer multibyte characters without further processing, to
+             * avoid getting confused in unsafe client encodings.
+             */
+            int            chlen = PQmblenBounded(word, pset.encoding);
+
+            while (chlen-- > 0)
+                *bptr++ = *word++;
+        }
+        else
+            *bptr++ = *word++;
+    }
+    *bptr++ = '%';
+    *bptr = '\0';
+
+    result = escape_string(buffer);
+    free(buffer);
+    return result;
+}
+
+
 /*
  * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
  *

Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
I wrote:
> [ v15 patch set ]

Sigh ... per the cfbot, this was already blindsided by 95787e849.
As I said, I don't want to sit on this for very long.

            regards, tom lane

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e09221d63d..e2026275b0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -46,7 +46,9 @@
 #include "catalog/pg_am_d.h"
 #include "catalog/pg_class_d.h"
 #include "common.h"
+#include "common/keywords.h"
 #include "libpq-fe.h"
+#include "mb/pg_wchar.h"
 #include "pqexpbuffer.h"
 #include "settings.h"
 #include "stringutils.h"
@@ -148,16 +150,16 @@ typedef struct SchemaQuery
     const char *namespace;

     /*
-     * Result --- the appropriately-quoted name to return, in the case of an
-     * unqualified name.  For example, "pg_catalog.quote_ident(c.relname)".
+     * Result --- the base object name to return.  For example, "c.relname".
      */
     const char *result;

     /*
-     * In some cases a different result must be used for qualified names.
-     * Enter that here, or write NULL if result can be used.
+     * Additional literal strings (usually keywords) to be offered along with
+     * the query results.  Provide a NULL-terminated array of constant
+     * strings, or NULL if none.
      */
-    const char *qualresult;
+    const char *const *keywords;
 } SchemaQuery;


@@ -179,6 +181,7 @@ static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
 static bool completion_case_sensitive;    /* completion is case sensitive */
+static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */

 /*
@@ -190,36 +193,87 @@ static bool completion_force_quote; /* true to force-quote filenames */
  *      We support both simple and versioned schema queries.
  * 3) The items from a null-pointer-terminated list (with or without
  *      case-sensitive comparison); if the list is constant you can build it
- *      with COMPLETE_WITH() or COMPLETE_WITH_CS().
+ *      with COMPLETE_WITH() or COMPLETE_WITH_CS().  The QUERY_LIST and
+ *      QUERY_PLUS forms combine such literal lists with a query result.
  * 4) The list of attributes of the given table (possibly schema-qualified).
  * 5) The list of arguments to the given function (possibly schema-qualified).
  */
 #define COMPLETE_WITH_QUERY(query) \
+    COMPLETE_WITH_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_QUERY_LIST(query, list) \
+do { \
+    completion_charp = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
+    matches = rl_completion_matches(text, complete_from_query); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_QUERY_VERBATIM(query) \
 do { \
     completion_charp = query; \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

 #define COMPLETE_WITH_VERSIONED_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list) \
 do { \
     completion_vquery = query; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_versioned_query); \
 } while (0)

-#define COMPLETE_WITH_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_VERSIONED_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = &(query); \
-    completion_charp = addon; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

-#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query, addon) \
+#define COMPLETE_WITH_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)
+
+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list) \
 do { \
     completion_squery = query; \
-    completion_vquery = addon; \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_versioned_schema_query); \
 } while (0)

+#define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_PLUS(query, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, list); \
+} while (0)
+
 /*
  * Caution: COMPLETE_WITH_CONST is not for general-purpose use; you probably
  * want COMPLETE_WITH() with one element, instead.
@@ -253,7 +307,10 @@ do { \
     COMPLETE_WITH_LIST_CS(list); \
 } while (0)

-#define COMPLETE_WITH_ATTR(relation, addon) \
+#define COMPLETE_WITH_ATTR(relation) \
+    COMPLETE_WITH_ATTR_LIST(relation, NULL)
+
+#define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
     char   *_completion_schema; \
     char   *_completion_table; \
@@ -266,18 +323,26 @@ do { \
                                 false, false, pset.encoding); \
     if (_completion_table == NULL) \
     { \
-        completion_charp = Query_for_list_of_attributes  addon; \
+        completion_charp = Query_for_list_of_attributes; \
         completion_info_charp = relation; \
     } \
     else \
     { \
-        completion_charp = Query_for_list_of_attributes_with_schema  addon; \
+        completion_charp = Query_for_list_of_attributes_with_schema; \
         completion_info_charp = _completion_table; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = list; \
+    completion_verbatim = false; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

+#define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
+do { \
+    static const char *const list[] = { __VA_ARGS__, NULL }; \
+    COMPLETE_WITH_ATTR_LIST(relation, list); \
+} while (0)
+
 /*
  * libedit will typically include the literal's leading single quote in
  * "text", while readline will not.  Adapt our offered strings to fit.
@@ -315,6 +380,8 @@ do { \
         completion_info_charp = _completion_type; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -340,6 +407,8 @@ do { \
         completion_info_charp = _completion_function; \
         completion_info_charp2 = _completion_schema; \
     } \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
     matches = rl_completion_matches(text, complete_from_query); \
 } while (0)

@@ -357,17 +426,45 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
         .selcondition = "p.prokind = 'a'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .selcondition = "p.proisagg",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

+static const char *const Keywords_for_list_of_datatypes[] = {
+    "bigint",
+    "boolean",
+    "character",
+    "double precision",
+    "integer",
+    "real",
+    "smallint",
+
+    /*
+     * Note: currently there's no value in offering the following multiword
+     * type names, because tab completion cannot succeed for them: we can't
+     * disambiguate until somewhere in the second word, at which point we
+     * won't have the first word as context.  ("double precision" does work,
+     * as long as no other type name begins with "double".)  Leave them out to
+     * encourage users to use the PG-specific aliases, which we can complete.
+     */
+#ifdef NOT_USED
+    "bit varying",
+    "character varying",
+    "time with time zone",
+    "time without time zone",
+    "timestamp with time zone",
+    "timestamp without time zone",
+#endif
+    NULL
+};
+
 static const SchemaQuery Query_for_list_of_datatypes = {
     .catname = "pg_catalog.pg_type t",
     /* selcondition --- ignore table rowtypes and array types */
@@ -377,8 +474,8 @@ static const SchemaQuery Query_for_list_of_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
+    .keywords = Keywords_for_list_of_datatypes,
 };

 static const SchemaQuery Query_for_list_of_composite_datatypes = {
@@ -389,8 +486,7 @@ static const SchemaQuery Query_for_list_of_composite_datatypes = {
     "AND t.typname !~ '^_'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.format_type(t.oid, NULL)",
-    .qualresult = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 static const SchemaQuery Query_for_list_of_domains = {
@@ -398,7 +494,7 @@ static const SchemaQuery Query_for_list_of_domains = {
     .selcondition = "t.typtype = 'd'",
     .viscondition = "pg_catalog.pg_type_is_visible(t.oid)",
     .namespace = "t.typnamespace",
-    .result = "pg_catalog.quote_ident(t.typname)",
+    .result = "t.typname",
 };

 /* Note: this intentionally accepts aggregates as well as plain functions */
@@ -409,13 +505,13 @@ static const SchemaQuery Query_for_list_of_functions[] = {
         .selcondition = "p.prokind != 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         .catname = "pg_catalog.pg_proc p",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     }
 };

@@ -426,7 +522,7 @@ static const SchemaQuery Query_for_list_of_procedures[] = {
         .selcondition = "p.prokind = 'p'",
         .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
         .namespace = "p.pronamespace",
-        .result = "pg_catalog.quote_ident(p.proname)",
+        .result = "p.proname",
     },
     {
         /* not supported in older versions */
@@ -438,7 +534,7 @@ static const SchemaQuery Query_for_list_of_routines = {
     .catname = "pg_catalog.pg_proc p",
     .viscondition = "pg_catalog.pg_function_is_visible(p.oid)",
     .namespace = "p.pronamespace",
-    .result = "pg_catalog.quote_ident(p.proname)",
+    .result = "p.proname",
 };

 static const SchemaQuery Query_for_list_of_sequences = {
@@ -446,7 +542,7 @@ static const SchemaQuery Query_for_list_of_sequences = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_SEQUENCE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_foreign_tables = {
@@ -454,7 +550,7 @@ static const SchemaQuery Query_for_list_of_foreign_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_tables = {
@@ -464,7 +560,7 @@ static const SchemaQuery Query_for_list_of_tables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_tables = {
@@ -472,7 +568,7 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_views = {
@@ -480,7 +576,7 @@ static const SchemaQuery Query_for_list_of_views = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_matviews = {
@@ -488,7 +584,7 @@ static const SchemaQuery Query_for_list_of_matviews = {
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_indexes = {
@@ -498,7 +594,7 @@ static const SchemaQuery Query_for_list_of_indexes = {
     CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_partitioned_indexes = {
@@ -506,7 +602,7 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
     .selcondition = "c.relkind = " CppAsString2(RELKIND_PARTITIONED_INDEX),
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };


@@ -515,7 +611,7 @@ static const SchemaQuery Query_for_list_of_relations = {
     .catname = "pg_catalog.pg_class c",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* partitioned relations */
@@ -525,14 +621,14 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = {
     ", " CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_operator_families = {
     .catname = "pg_catalog.pg_opfamily c",
     .viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)",
     .namespace = "c.opfnamespace",
-    .result = "pg_catalog.quote_ident(c.opfname)",
+    .result = "c.opfname",
 };

 /* Relations supporting INSERT, UPDATE or DELETE */
@@ -545,7 +641,7 @@ static const SchemaQuery Query_for_list_of_updatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting SELECT */
@@ -560,7 +656,7 @@ static const SchemaQuery Query_for_list_of_selectables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting TRUNCATE */
@@ -572,7 +668,7 @@ static const SchemaQuery Query_for_list_of_truncatables = {
     CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting GRANT are currently same as those supporting SELECT */
@@ -588,7 +684,7 @@ static const SchemaQuery Query_for_list_of_analyzables = {
     CppAsString2(RELKIND_FOREIGN_TABLE) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /* Relations supporting index creation */
@@ -600,7 +696,7 @@ static const SchemaQuery Query_for_list_of_indexables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 /*
@@ -617,7 +713,7 @@ static const SchemaQuery Query_for_list_of_clusterables = {
     CppAsString2(RELKIND_MATVIEW) ")",
     .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
     .namespace = "c.relnamespace",
-    .result = "pg_catalog.quote_ident(c.relname)",
+    .result = "c.relname",
 };

 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
@@ -625,14 +721,14 @@ static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .selcondition = "c.conrelid <> 0",
     .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
-    .result = "pg_catalog.quote_ident(c.conname)",
+    .result = "c.conname",
 };

 static const SchemaQuery Query_for_list_of_statistics = {
     .catname = "pg_catalog.pg_statistic_ext s",
     .viscondition = "pg_catalog.pg_statistics_obj_is_visible(s.oid)",
     .namespace = "s.stxnamespace",
-    .result = "pg_catalog.quote_ident(s.stxname)",
+    .result = "s.stxname",
 };

 static const SchemaQuery Query_for_list_of_collations = {
@@ -640,7 +736,7 @@ static const SchemaQuery Query_for_list_of_collations = {
     .selcondition = "c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))",
     .viscondition = "pg_catalog.pg_collation_is_visible(c.oid)",
     .namespace = "c.collnamespace",
-    .result = "pg_catalog.quote_ident(c.collname)",
+    .result = "c.collname",
 };


@@ -659,13 +755,13 @@ static const SchemaQuery Query_for_list_of_collations = {
  */

 #define Query_for_list_of_attributes \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
 " WHERE c.oid = a.attrelid "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

@@ -676,21 +772,21 @@ static const SchemaQuery Query_for_list_of_collations = {
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
 "   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"'='%s') "\
 "   AND pg_catalog.pg_table_is_visible(c.oid)"

 #define Query_for_list_of_attributes_with_schema \
-"SELECT pg_catalog.quote_ident(attname) "\
+"SELECT attname "\
 "  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
 " WHERE c.oid = a.attrelid "\
 "   AND n.oid = c.relnamespace "\
 "   AND a.attnum > 0 "\
 "   AND NOT a.attisdropped "\
-"   AND substring(pg_catalog.quote_ident(attname),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(relname)='%s' "\
+"   AND substring(attname,1,%d)='%s' "\
+"   AND (relname='%s' "\
 "        OR '\"' || relname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_quoted \
@@ -698,7 +794,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  FROM pg_catalog.pg_enum e, pg_catalog.pg_type t "\
 " WHERE t.oid = e.enumtypid "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -707,7 +803,7 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  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' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
 "   AND pg_catalog.pg_type_is_visible(t.oid)"

@@ -717,9 +813,9 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_enum_values_with_schema_unquoted \
@@ -728,24 +824,24 @@ static const SchemaQuery Query_for_list_of_collations = {
 " WHERE t.oid = e.enumtypid "\
 "   AND n.oid = t.typnamespace "\
 "   AND substring(enumlabel,1,%d)='%s' "\
-"   AND (pg_catalog.quote_ident(typname)='%s' "\
+"   AND (typname='%s' "\
 "        OR '\"' || typname || '\"'='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_template_databases \
-"SELECT pg_catalog.quote_ident(d.datname) "\
+"SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(pg_catalog.quote_ident(d.datname),1,%d)='%s' "\
+" WHERE substring(d.datname,1,%d)='%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
-"SELECT pg_catalog.quote_ident(datname) FROM pg_catalog.pg_database "\
-" WHERE substring(pg_catalog.quote_ident(datname),1,%d)='%s'"
+"SELECT datname FROM pg_catalog.pg_database "\
+" WHERE substring(datname,1,%d)='%s'"

 #define Query_for_list_of_tablespaces \
-"SELECT pg_catalog.quote_ident(spcname) FROM pg_catalog.pg_tablespace "\
-" WHERE substring(pg_catalog.quote_ident(spcname),1,%d)='%s'"
+"SELECT spcname FROM pg_catalog.pg_tablespace "\
+" WHERE substring(spcname,1,%d)='%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
@@ -753,287 +849,260 @@ static const SchemaQuery Query_for_list_of_collations = {
 "  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"

 #define Query_for_list_of_languages \
-"SELECT pg_catalog.quote_ident(lanname) "\
+"SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(pg_catalog.quote_ident(lanname),1,%d)='%s'"
+"   AND substring(lanname,1,%d)='%s'"

 #define Query_for_list_of_schemas \
-"SELECT pg_catalog.quote_ident(nspname) FROM pg_catalog.pg_namespace "\
-" WHERE substring(pg_catalog.quote_ident(nspname),1,%d)='%s'"
+"SELECT nspname FROM pg_catalog.pg_namespace "\
+" WHERE substring(nspname,1,%d)='%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context != 'internal' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context IN ('user', 'superuser') "\
-"  UNION ALL SELECT 'constraints' "\
-"  UNION ALL SELECT 'transaction' "\
-"  UNION ALL SELECT 'session' "\
-"  UNION ALL SELECT 'role' "\
-"  UNION ALL SELECT 'tablespace' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
-"  UNION ALL SELECT 'session authorization' "\
-"  UNION ALL SELECT 'all') ss "\
+" ) ss "\
 " WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
+" SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"
+"  WHERE substring(rolname,1,%d)='%s'"

-#define Query_for_list_of_grant_roles \
-" SELECT pg_catalog.quote_ident(rolname) "\
-"   FROM pg_catalog.pg_roles "\
-"  WHERE substring(pg_catalog.quote_ident(rolname),1,%d)='%s'"\
-" UNION ALL SELECT 'PUBLIC'"\
-" UNION ALL SELECT 'CURRENT_ROLE'"\
-" UNION ALL SELECT 'CURRENT_USER'"\
-" UNION ALL SELECT 'SESSION_USER'"
+/* add these to Query_for_list_of_roles in GRANT contexts */
+#define Keywords_for_list_of_grant_roles \
+"PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_index_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
 " WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"

 #define Query_for_unique_index_of_table \
 Query_for_index_of_table \
 "       and i.indisunique"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_table_not_validated \
-"SELECT pg_catalog.quote_ident(conname) "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)" \
+Query_for_constraint_of_table \
 "       and not con.convalidated"

 #define Query_for_all_table_constraints \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
-" WHERE c.conrelid <> 0 "
+" WHERE c.conrelid <> 0 "\
+"       and substring(conname,1,%d)='%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_constraint_of_type \
-"SELECT pg_catalog.quote_ident(conname) "\
+"SELECT conname "\
 "  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(t.typname)='%s'"\
+" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
+"       and t.typname='%s'"\
 "       and pg_catalog.pg_type_is_visible(t.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_constraint \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE pg_catalog.quote_ident(conname)='%s')"
+"         WHERE conname='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_rule_of_table \
-"SELECT pg_catalog.quote_ident(rulename) "\
+"SELECT rulename "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_rule \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE pg_catalog.quote_ident(rulename)='%s')"
+"         WHERE rulename='%s')"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_trigger_of_table \
-"SELECT pg_catalog.quote_ident(tgname) "\
+"SELECT tgname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c1.oid)"\
 "       and not tgisinternal"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_tables_for_trigger \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE pg_catalog.quote_ident(tgname)='%s')"
+"         WHERE tgname='%s')"

 #define Query_for_list_of_ts_configurations \
-"SELECT pg_catalog.quote_ident(cfgname) FROM pg_catalog.pg_ts_config "\
-" WHERE substring(pg_catalog.quote_ident(cfgname),1,%d)='%s'"
+"SELECT cfgname FROM pg_catalog.pg_ts_config "\
+" WHERE substring(cfgname,1,%d)='%s'"

 #define Query_for_list_of_ts_dictionaries \
-"SELECT pg_catalog.quote_ident(dictname) FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(pg_catalog.quote_ident(dictname),1,%d)='%s'"
+"SELECT dictname FROM pg_catalog.pg_ts_dict "\
+" WHERE substring(dictname,1,%d)='%s'"

 #define Query_for_list_of_ts_parsers \
-"SELECT pg_catalog.quote_ident(prsname) FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(pg_catalog.quote_ident(prsname),1,%d)='%s'"
+"SELECT prsname FROM pg_catalog.pg_ts_parser "\
+" WHERE substring(prsname,1,%d)='%s'"

 #define Query_for_list_of_ts_templates \
-"SELECT pg_catalog.quote_ident(tmplname) FROM pg_catalog.pg_ts_template "\
-" WHERE substring(pg_catalog.quote_ident(tmplname),1,%d)='%s'"
+"SELECT tmplname FROM pg_catalog.pg_ts_template "\
+" WHERE substring(tmplname,1,%d)='%s'"

 #define Query_for_list_of_fdws \
-" SELECT pg_catalog.quote_ident(fdwname) "\
+" SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(pg_catalog.quote_ident(fdwname),1,%d)='%s'"
+"  WHERE substring(fdwname,1,%d)='%s'"

 #define Query_for_list_of_servers \
-" SELECT pg_catalog.quote_ident(srvname) "\
+" SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(pg_catalog.quote_ident(srvname),1,%d)='%s'"
+"  WHERE substring(srvname,1,%d)='%s'"

 #define Query_for_list_of_user_mappings \
-" SELECT pg_catalog.quote_ident(usename) "\
+" SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(pg_catalog.quote_ident(usename),1,%d)='%s'"
+"  WHERE substring(usename,1,%d)='%s'"

 #define Query_for_list_of_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s'"
+"  WHERE substring(amname,1,%d)='%s'"

 #define Query_for_list_of_index_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
-" SELECT pg_catalog.quote_ident(amname) "\
+" SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(pg_catalog.quote_ident(amname),1,%d)='%s' AND "\
+"  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc "\
-" WHERE (%d = pg_catalog.length('%s'))"\
-"   AND (pg_catalog.quote_ident(proname)='%s'"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
+"   AND (proname='%s'"\
 "        OR '\"' || proname || '\"'='%s') "\
 "   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_list_of_arguments_with_schema \
 "SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
 "  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
 "   AND n.oid = p.pronamespace "\
-"   AND (pg_catalog.quote_ident(proname)='%s' "\
+"   AND (proname='%s' "\
 "        OR '\"' || proname || '\"' ='%s') "\
-"   AND (pg_catalog.quote_ident(nspname)='%s' "\
+"   AND (nspname='%s' "\
 "        OR '\"' || nspname || '\"' ='%s') "

 #define Query_for_list_of_extensions \
-" SELECT pg_catalog.quote_ident(extname) "\
+" SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(pg_catalog.quote_ident(extname),1,%d)='%s'"
+"  WHERE substring(extname,1,%d)='%s'"

 #define Query_for_list_of_available_extensions \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s' AND installed_version IS NULL"
+"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions \
-" SELECT pg_catalog.quote_ident(version) "\
+" SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring(version,1,%d)='%s'"\
+"    AND name='%s'"

-/* the silly-looking length condition is just to eat up the current word */
+/* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions_with_TO \
-" SELECT 'TO ' || pg_catalog.quote_ident(version) "\
+" SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE (%d = pg_catalog.length('%s'))"\
-"    AND pg_catalog.quote_ident(name)='%s'"
+"  WHERE substring('TO ' || version,1,%d)='%s'"\
+"    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 #define Query_for_list_of_event_triggers \
-" SELECT pg_catalog.quote_ident(evtname) "\
+" SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(pg_catalog.quote_ident(evtname),1,%d)='%s'"
+"  WHERE substring(evtname,1,%d)='%s'"

 #define Query_for_list_of_tablesample_methods \
-" SELECT pg_catalog.quote_ident(proname) "\
+" SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(pg_catalog.quote_ident(proname),1,%d)='%s'"
+"        substring(proname,1,%d)='%s'"

 #define Query_for_list_of_policies \
-" SELECT pg_catalog.quote_ident(polname) "\
+" SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(pg_catalog.quote_ident(polname),1,%d)='%s'"
+"  WHERE substring(polname,1,%d)='%s'"

 #define Query_for_list_of_tables_for_policy \
-"SELECT pg_catalog.quote_ident(relname) "\
+"SELECT relname "\
 "  FROM pg_catalog.pg_class"\
-" WHERE (%d = pg_catalog.length('%s'))"\
+" WHERE substring(relname,1,%d)='%s'"\
 "   AND oid IN "\
 "       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE pg_catalog.quote_ident(polname)='%s')"
+"         WHERE polname='%s')"

 #define Query_for_enum \
 " SELECT name FROM ( "\
-"   SELECT pg_catalog.quote_ident(pg_catalog.unnest(enumvals)) AS name "\
+"   SELECT pg_catalog.unnest(enumvals) AS name "\
 "     FROM pg_catalog.pg_settings "\
 "    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
-"    UNION ALL " \
-"   SELECT 'DEFAULT' ) ss "\
+"    ) ss "\
 "  WHERE pg_catalog.substring(name,1,%%d)='%%s'"

-/* the silly-looking length condition is just to eat up the current word */
 #define Query_for_partition_of_table \
-"SELECT pg_catalog.quote_ident(c2.relname) "\
+"SELECT c2.relname "\
 "  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
 " WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and (%d = pg_catalog.length('%s'))"\
-"       and pg_catalog.quote_ident(c1.relname)='%s'"\
+"       and substring(c2.relname,1,%d)='%s'"\
+"       and c1.relname='%s'"\
 "       and pg_catalog.pg_table_is_visible(c2.oid)"\
 "       and c2.relispartition = 'true'"

 #define Query_for_list_of_cursors \
-" SELECT pg_catalog.quote_ident(name) "\
+" SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(pg_catalog.quote_ident(name),1,%d)='%s'"
+"  WHERE substring(name,1,%d)='%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1043,18 +1112,18 @@ Query_for_index_of_table \

 static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(pubname) "
+        " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pg_catalog.quote_ident(pubname),1,%d)='%s'"
+        "  WHERE substring(pubname,1,%d)='%s'"
     },
     {0, NULL}
 };

 static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
-        " SELECT pg_catalog.quote_ident(s.subname) "
+        " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(pg_catalog.quote_ident(s.subname),1,%d)='%s' "
+        "  WHERE substring(s.subname,1,%d)='%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1069,9 +1138,11 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
 typedef struct
 {
     const char *name;
+    /* Provide at most one of these three types of query: */
     const char *query;            /* simple query, or NULL */
     const VersionedQuery *vquery;    /* versioned query, or NULL */
     const SchemaQuery *squery;    /* schema query, or NULL */
+    const char *const *keywords;    /* keywords to be offered as well */
     const bits32 flags;            /* visibility flags, see below */
 } pgsql_thing_t;

@@ -1080,8 +1151,14 @@ typedef struct
 #define THING_NO_ALTER        (1 << 2)    /* should not show up after ALTER */
 #define THING_NO_SHOW        (THING_NO_CREATE | THING_NO_DROP | THING_NO_ALTER)

+/* When we have DROP USER etc, also offer MAPPING FOR */
+static const char *const Keywords_for_user_thing[] = {
+    "MAPPING FOR",
+    NULL
+};
+
 static const pgsql_thing_t words_after_create[] = {
-    {"ACCESS METHOD", NULL, NULL, NULL, THING_NO_ALTER},
+    {"ACCESS METHOD", NULL, NULL, NULL, NULL, THING_NO_ALTER},
     {"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates},
     {"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
                                  * skip it */
@@ -1091,11 +1168,11 @@ static const pgsql_thing_t words_after_create[] = {
      * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
      * to be used only by pg_dump.
      */
-    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT pg_catalog.quote_ident(conname) FROM pg_catalog.pg_conversion WHERE
substring(pg_catalog.quote_ident(conname),1,%d)='%s'"},
+    {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, NULL, THING_NO_SHOW},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
     {"DATABASE", Query_for_list_of_databases},
-    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
-    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, THING_NO_SHOW},
+    {"DEFAULT PRIVILEGES", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, NULL, THING_NO_SHOW},
     {"DOMAIN", NULL, NULL, &Query_for_list_of_domains},
     {"EVENT TRIGGER", NULL, NULL, NULL},
     {"EXTENSION", Query_for_list_of_extensions},
@@ -1105,41 +1182,41 @@ static const pgsql_thing_t words_after_create[] = {
     {"GROUP", Query_for_list_of_roles},
     {"INDEX", NULL, NULL, &Query_for_list_of_indexes},
     {"LANGUAGE", Query_for_list_of_languages},
-    {"LARGE OBJECT", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews},
     {"OPERATOR", NULL, NULL, NULL}, /* Querying for this is probably not such
                                      * a good idea. */
-    {"OR REPLACE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
-    {"OWNED", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
-    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, THING_NO_SHOW},
+    {"OR REPLACE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},
+    {"OWNED", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},    /* for DROP OWNED BY ... */
+    {"PARSER", Query_for_list_of_ts_parsers, NULL, NULL, NULL, THING_NO_SHOW},
     {"POLICY", NULL, NULL, NULL},
     {"PROCEDURE", NULL, NULL, Query_for_list_of_procedures},
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
-    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, THING_NO_CREATE},
-    {"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE
substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
+    {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
     {"STATISTICS", NULL, NULL, &Query_for_list_of_statistics},
     {"SUBSCRIPTION", NULL, Query_for_list_of_subscriptions},
-    {"SYSTEM", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+    {"SYSTEM", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"TABLE", NULL, NULL, &Query_for_list_of_tables},
     {"TABLESPACE", Query_for_list_of_tablespaces},
-    {"TEMP", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TEMP TABLE
-                                                                 * ... */
-    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, THING_NO_SHOW},
-    {"TEMPORARY", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
-                                                                         * TABLE ... */
+    {"TEMP", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMP TABLE
+                                                                         * ... */
+    {"TEMPLATE", Query_for_list_of_ts_templates, NULL, NULL, NULL, THING_NO_SHOW},
+    {"TEMPORARY", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE TEMPORARY
+                                                                             * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
-    {"TRANSFORM", NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT pg_catalog.quote_ident(tgname) FROM pg_catalog.pg_trigger WHERE
substring(pg_catalog.quote_ident(tgname),1,%d)='%s'AND NOT tgisinternal"}, 
+    {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
-    {"UNIQUE", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNIQUE
-                                                                     * INDEX ... */
-    {"UNLOGGED", NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNLOGGED
-                                                                     * TABLE ... */
-    {"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"},
+    {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
+                                                                         * INDEX ... */
+    {"UNLOGGED", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},    /* for CREATE UNLOGGED
+                                                                             * TABLE ... */
+    {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
     {"USER MAPPING FOR", NULL, NULL, NULL},
     {"VIEW", NULL, NULL, &Query_for_list_of_views},
     {NULL}                        /* end of list */
@@ -1200,6 +1277,8 @@ static char *complete_from_schema_query(const char *text, int state);
 static char *complete_from_versioned_schema_query(const char *text, int state);
 static char *_complete_from_query(const char *simple_query,
                                   const SchemaQuery *schema_query,
+                                  const char *const *keywords,
+                                  bool verbatim,
                                   const char *text, int state);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
@@ -1212,6 +1291,12 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static void parse_identifier(const char *ident,
+                             char **schemaname, char **objectname,
+                             bool *schemaquoted, bool *objectquoted);
+static char *requote_identifier(const char *schemaname, const char *objectname,
+                                bool quote_schema, bool quote_object);
+static bool identifier_needs_quotes(const char *ident);
 static PGresult *exec_query(const char *query);

 static char **get_previous_words(int point, char **buffer, int *nwords);
@@ -1650,8 +1735,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE */
     else if (Matches("ALTER", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ALL IN TABLESPACE");

     /* ALTER something */
     else if (Matches("ALTER"))
@@ -1695,7 +1780,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") ||
              (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") &&
               ends_with(prev_wd, ',')))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE"))
         COMPLETE_WITH(",");
     /* ALTER PUBLICATION <name> DROP */
@@ -1705,11 +1790,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET"))
         COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     /* ALTER PUBLICATION <name> SET ( */
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny) && TailMatches("SET", "("))
         COMPLETE_WITH("publish", "publish_via_partition_root");
@@ -1788,14 +1873,14 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions_with_TO);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
         completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* ALTER FOREIGN */
@@ -1817,8 +1902,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER INDEX */
     else if (Matches("ALTER", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ALL IN TABLESPACE");
     /* ALTER INDEX <name> */
     else if (Matches("ALTER", "INDEX", MatchAny))
         COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME TO", "SET",
@@ -1827,7 +1912,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH"))
         COMPLETE_WITH("PARTITION");
     else if (Matches("ALTER", "INDEX", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     /* ALTER INDEX <name> ALTER */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER"))
         COMPLETE_WITH("COLUMN");
@@ -1884,8 +1969,8 @@ psql_completion(const char *text, int start, int end)

     /* ALTER MATERIALIZED VIEW */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   "UNION SELECT 'ALL IN TABLESPACE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "ALL IN TABLESPACE");

     /* ALTER USER,ROLE <name> */
     else if (Matches("ALTER", "USER|ROLE", MatchAny) &&
@@ -1979,7 +2064,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "SYSTEM"))
         COMPLETE_WITH("SET", "RESET");
     else if (Matches("ALTER", "SYSTEM", "SET|RESET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_alter_system_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_alter_system_set_vars,
+                                 "all");
     else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
         COMPLETE_WITH("TO");
     /* ALTER VIEW <name> */
@@ -1988,9 +2074,9 @@ psql_completion(const char *text, int start, int end)
                       "SET SCHEMA");
     /* ALTER VIEW xxx RENAME */
     else if (Matches("ALTER", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER VIEW xxx ALTER [ COLUMN ] yyy */
     else if (Matches("ALTER", "VIEW", MatchAny, "ALTER", MatchAny) ||
              Matches("ALTER", "VIEW", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2009,9 +2095,9 @@ psql_completion(const char *text, int start, int end)
                       "RESET (", "SET");
     /* ALTER MATERIALIZED VIEW xxx RENAME */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "TO");
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER MATERIALIZED VIEW xxx RENAME yyy */
     else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "RENAME", MatchAnyExcept("TO")))
         COMPLETE_WITH("TO");
@@ -2038,7 +2124,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RENAME TO", "TO", "USING (", "WITH CHECK (");
     /* ALTER POLICY <name> ON <table> TO <role> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* ALTER POLICY <name> ON <table> USING ( */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2079,7 +2166,7 @@ psql_completion(const char *text, int start, int end)
      * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
      */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
@@ -2106,7 +2193,7 @@ psql_completion(const char *text, int start, int end)
     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")))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     /* ALTER TABLE xxx ADD CONSTRAINT yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
         COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY");
@@ -2170,13 +2257,13 @@ psql_completion(const char *text, int start, int end)
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx NO */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO"))
         COMPLETE_WITH("FORCE ROW LEVEL SECURITY", "INHERIT");
     /* ALTER TABLE xxx NO INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "NO", "INHERIT"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* ALTER TABLE xxx DISABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE"))
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
@@ -2193,13 +2280,13 @@ psql_completion(const char *text, int start, int end)

     /* ALTER TABLE xxx ALTER */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT");

     /* ALTER TABLE xxx RENAME */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME"))
-        COMPLETE_WITH_ATTR(prev2_wd, " UNION SELECT 'COLUMN' UNION SELECT 'CONSTRAINT' UNION SELECT 'TO'");
+        COMPLETE_WITH_ATTR_PLUS(prev2_wd, "COLUMN", "CONSTRAINT", "TO");
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|RENAME", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

     /* ALTER TABLE xxx RENAME yyy */
     else if (Matches("ALTER", "TABLE", MatchAny, "RENAME", MatchAnyExcept("CONSTRAINT|TO")))
@@ -2214,7 +2301,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("COLUMN", "CONSTRAINT");
     /* If we have ALTER TABLE <sth> DROP COLUMN, provide list of columns */
     else if (Matches("ALTER", "TABLE", MatchAny, "DROP", "COLUMN"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
@@ -2304,7 +2391,7 @@ psql_completion(const char *text, int start, int end)
      * tables.
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "ATTACH", "PARTITION"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("ATTACH", "PARTITION", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2366,7 +2453,7 @@ psql_completion(const char *text, int start, int end)
      * of attributes
      */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER|DROP|RENAME", "ATTRIBUTE"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);
     /* ALTER TYPE ALTER ATTRIBUTE <foo> */
     else if (Matches("ALTER", "TYPE", MatchAny, "ALTER", "ATTRIBUTE", MatchAny))
         COMPLETE_WITH("TYPE");
@@ -2391,8 +2478,8 @@ psql_completion(const char *text, int start, int end)
  * ANALYZE [ VERBOSE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables,
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_analyzables,
+                                        "VERBOSE");
     else if (HeadMatches("ANALYZE", "(*") &&
              !HeadMatches("ANALYZE", "(*)"))
     {
@@ -2408,9 +2495,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("ANALYZE") && TailMatches("("))
         /* "ANALYZE (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("ANALYZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_analyzables);

 /* BEGIN */
     else if (Matches("BEGIN"))
@@ -2431,19 +2518,20 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("CHAIN");
 /* CALL */
     else if (Matches("CALL"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
     else if (Matches("CALL", MatchAny))
         COMPLETE_WITH("(");
 /* CLOSE */
     else if (Matches("CLOSE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL");
 /* CLUSTER */
     else if (Matches("CLUSTER"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, "UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables,
+                                        "VERBOSE");
     else if (Matches("CLUSTER", "VERBOSE") ||
              Matches("CLUSTER", "(*)"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables);
     /* If we have CLUSTER <sth>, then add "USING" */
     else if (Matches("CLUSTER", MatchAnyExcept("VERBOSE|ON|(|(*)")))
         COMPLETE_WITH("USING");
@@ -2493,19 +2581,19 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_constraint
-                            " UNION SELECT 'DOMAIN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                 "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (Matches("COMMENT", "ON", "EVENT", "TRIGGER"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);
     else if (Matches("COMMENT", "ON", "FOREIGN"))
         COMPLETE_WITH("DATA WRAPPER", "TABLE");
     else if (Matches("COMMENT", "ON", "FOREIGN", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (Matches("COMMENT", "ON", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("COMMENT", "ON", "POLICY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_policies);
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny))
@@ -2535,7 +2623,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH", "TEMPLATE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_templates);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -2563,8 +2651,7 @@ psql_completion(const char *text, int start, int end)
      * backslash command).
      */
     else if (Matches("COPY|\\copy"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION ALL SELECT '('");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables, "(");
     /* Complete COPY ( with legal query commands */
     else if (Matches("COPY|\\copy", "("))
         COMPLETE_WITH("SELECT", "TABLE", "VALUES", "INSERT INTO", "UPDATE", "DELETE FROM", "WITH");
@@ -2622,7 +2709,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "COLLATION", MatchAny))
         COMPLETE_WITH("(", "FROM");
     else if (Matches("CREATE", "COLLATION", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);
     else if (HeadMatches("CREATE", "COLLATION", MatchAny, "(*"))
     {
         if (TailMatches("(|*,"))
@@ -2648,12 +2735,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "DOMAIN", MatchAny))
         COMPLETE_WITH("AS");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "DOMAIN", MatchAny, "AS", MatchAny))
         COMPLETE_WITH("COLLATE", "DEFAULT", "CONSTRAINT",
                       "NOT NULL", "NULL", "CHECK (");
     else if (Matches("CREATE", "DOMAIN", MatchAny, "COLLATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations);

     /* CREATE EXTENSION */
     /* Complete with available extensions rather than installed ones. */
@@ -2666,7 +2753,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
         completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_available_extension_versions);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

     /* CREATE FOREIGN */
@@ -2691,9 +2778,8 @@ psql_completion(const char *text, int start, int end)
      * existing indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'"
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON", "CONCURRENTLY");

     /*
      * Complete ... INDEX|CONCURRENTLY [<name>] ON with a list of relations
@@ -2701,15 +2787,15 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("INDEX|CONCURRENTLY", MatchAny, "ON") ||
              TailMatches("INDEX|CONCURRENTLY", "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);

     /*
      * Complete CREATE|UNIQUE INDEX CONCURRENTLY with "ON" and existing
      * indexes
      */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'ON'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "ON");
     /* Complete CREATE|UNIQUE INDEX [CONCURRENTLY] <sth> with "ON" */
     else if (TailMatches("CREATE|UNIQUE", "INDEX", MatchAny) ||
              TailMatches("CREATE|UNIQUE", "INDEX", "CONCURRENTLY", MatchAny))
@@ -2724,10 +2810,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("(", "USING");
     else if (TailMatches("INDEX", MatchAny, "ON", MatchAny, "(") ||
              TailMatches("INDEX|CONCURRENTLY", "ON", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* same if you put in USING */
     else if (TailMatches("ON", MatchAny, "USING", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev4_wd, "");
+        COMPLETE_WITH_ATTR(prev4_wd);
     /* Complete USING with an index method */
     else if (TailMatches("INDEX", MatchAny, MatchAny, "ON", MatchAny, "USING") ||
              TailMatches("INDEX", MatchAny, "ON", MatchAny, "USING") ||
@@ -2748,7 +2834,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     /* Complete "CREATE POLICY <name> ON <table>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* Complete "CREATE POLICY <name> ON <table> AS|FOR|TO|USING|WITH CHECK" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("AS", "FOR", "TO", "USING (", "WITH CHECK (");
@@ -2776,7 +2862,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO", "USING (", "WITH CHECK (");
     /* Complete "CREATE POLICY <name> ON <table> TO <role>" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "CREATE POLICY <name> ON <table> USING (" */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "USING"))
         COMPLETE_WITH("(");
@@ -2814,7 +2901,8 @@ psql_completion(const char *text, int start, int end)
      * <role>"
      */
     else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "AS", MatchAny, "TO"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);

     /*
      * Complete "CREATE POLICY <name> ON <table> AS PERMISSIVE|RESTRICTIVE
@@ -2837,18 +2925,18 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> FOR TABLE" with "<table>, ..." */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete "CREATE PUBLICATION <name> FOR ALL TABLES IN SCHEMA <schema>,
      * ..."
      */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " AND nspname != 'pg_catalog' "
-                            " AND nspname not like 'pg\\_toast%%' "
-                            " AND nspname not like 'pg\\_temp%%' "
-                            " UNION SELECT 'CURRENT_SCHEMA' ");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                 " AND nspname != 'pg_catalog' "
+                                 " AND nspname not like 'pg\\_toast%%' "
+                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 "CURRENT_SCHEMA");
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) &&
(!ends_with(prev_wd,','))) 
         COMPLETE_WITH("WITH (");
     /* Complete "CREATE PUBLICATION <name> [...] WITH" */
@@ -2877,7 +2965,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TO");
     /* Complete "AS ON <sth> TO" with a table name */
     else if (TailMatches("AS", "ON", "SELECT|UPDATE|INSERT|DELETE", "TO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE SEQUENCE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     else if (TailMatches("CREATE", "SEQUENCE", MatchAny) ||
@@ -2904,7 +2992,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (HeadMatches("CREATE", "STATISTICS", MatchAny) &&
              TailMatches("FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
     /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
@@ -2918,7 +3006,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RANGE (", "LIST (", "HASH (");
     /* If we have xxx PARTITION OF, provide a list of partitioned tables */
     else if (TailMatches("PARTITION", "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, "");
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     /* Limited completion support for partition bound specification */
     else if (TailMatches("PARTITION", "OF", MatchAny))
         COMPLETE_WITH("FOR VALUES", "DEFAULT");
@@ -2929,7 +3017,7 @@ psql_completion(const char *text, int start, int end)
     /* Complete CREATE TABLE <name> OF with list of composite types */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "OF") ||
              TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
     /* Complete CREATE TABLE name (...) with supported options */
     else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)") ||
              TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)"))
@@ -2968,7 +3056,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "TRANSFORM", "FOR") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny) ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
@@ -3036,7 +3124,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "BEFORE|AFTER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);

     /*
      * Complete CREATE [ OR REPLACE ] TRIGGER ... INSTEAD OF event ON with a
@@ -3044,7 +3132,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (TailMatches("CREATE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON") ||
              TailMatches("CREATE", "OR", "REPLACE", "TRIGGER", MatchAny, "INSTEAD", "OF", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("ON", MatchAny))
@@ -3149,7 +3237,7 @@ psql_completion(const char *text, int start, int end)
     else if ((HeadMatches("CREATE", "TRIGGER") ||
               HeadMatches("CREATE", "OR", "REPLACE", "TRIGGER")) &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* CREATE ROLE,USER,GROUP <name> */
     else if (Matches("CREATE", "ROLE|GROUP|USER", MatchAny) &&
@@ -3185,7 +3273,7 @@ psql_completion(const char *text, int start, int end)
     else if (HeadMatches("CREATE", "TYPE", MatchAny, "AS", "("))
     {
         if (TailMatches("(|*,", MatchAny))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("(|*,", MatchAny, MatchAnyExcept("*)")))
             COMPLETE_WITH("COLLATE", ",", ")");
     }
@@ -3269,12 +3357,12 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("CREATE", "EVENT", "TRIGGER") &&
              TailMatches("EXECUTE", "FUNCTION|PROCEDURE"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);

 /* DEALLOCATE */
     else if (Matches("DEALLOCATE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements
-                            " UNION SELECT 'ALL'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_prepared_statements,
+                                 "ALL");

 /* DECLARE */

@@ -3322,7 +3410,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("FROM");
     /* Complete DELETE FROM with a list of tables */
     else if (TailMatches("DELETE", "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete DELETE FROM <table> */
     else if (TailMatches("DELETE", "FROM", MatchAny))
         COMPLETE_WITH("USING", "WHERE");
@@ -3364,10 +3452,10 @@ psql_completion(const char *text, int start, int end)

     /* DROP INDEX */
     else if (Matches("DROP", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("DROP", "INDEX", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
     else if (Matches("DROP", "INDEX", "CONCURRENTLY", MatchAny))
@@ -3377,7 +3465,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("DROP", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("DROP", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");

@@ -3446,7 +3534,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("DROP", "TRANSFORM"))
         COMPLETE_WITH("FOR");
     else if (Matches("DROP", "TRANSFORM", "FOR"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny))
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
@@ -3500,28 +3588,28 @@ psql_completion(const char *text, int start, int end)
      * NEXT, PRIOR, FIRST, LAST, FROM, IN, and a list of cursors
      */
     else if (Matches("FETCH|MOVE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ABSOLUTE'"
-                            " UNION SELECT 'BACKWARD'"
-                            " UNION SELECT 'FORWARD'"
-                            " UNION SELECT 'RELATIVE'"
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'NEXT'"
-                            " UNION SELECT 'PRIOR'"
-                            " UNION SELECT 'FIRST'"
-                            " UNION SELECT 'LAST'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ABSOLUTE",
+                                 "BACKWARD",
+                                 "FORWARD",
+                                 "RELATIVE",
+                                 "ALL",
+                                 "NEXT",
+                                 "PRIOR",
+                                 "FIRST",
+                                 "LAST",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH BACKWARD or FORWARD with one of ALL, FROM, IN, and a
      * list of cursors
      */
     else if (Matches("FETCH|MOVE", "BACKWARD|FORWARD"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'ALL'"
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "ALL",
+                                 "FROM",
+                                 "IN");

     /*
      * Complete FETCH <direction> with "FROM" or "IN". These are equivalent,
@@ -3531,9 +3619,9 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("FETCH|MOVE", "ABSOLUTE|BACKWARD|FORWARD|RELATIVE",
                      MatchAnyExcept("FROM|IN")) ||
              Matches("FETCH|MOVE", "ALL|NEXT|PRIOR|FIRST|LAST"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_cursors
-                            " UNION SELECT 'FROM'"
-                            " UNION SELECT 'IN'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors,
+                                 "FROM",
+                                 "IN");
     /* Complete FETCH <direction> "FROM" or "IN" with a list of cursors */
     else if (HeadMatches("FETCH|MOVE") &&
              TailMatches("FROM|IN"))
@@ -3552,7 +3640,7 @@ psql_completion(const char *text, int start, int end)
 /* FOREIGN TABLE */
     else if (TailMatches("FOREIGN", "TABLE") &&
              !TailMatches("CREATE", MatchAny, MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);

 /* FOREIGN SERVER */
     else if (TailMatches("FOREIGN", "SERVER"))
@@ -3574,20 +3662,20 @@ psql_completion(const char *text, int start, int end)
                           "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
                           "EXECUTE", "USAGE", "ALL");
         else
-            COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                                " UNION SELECT 'SELECT'"
-                                " UNION SELECT 'INSERT'"
-                                " UNION SELECT 'UPDATE'"
-                                " UNION SELECT 'DELETE'"
-                                " UNION SELECT 'TRUNCATE'"
-                                " UNION SELECT 'REFERENCES'"
-                                " UNION SELECT 'TRIGGER'"
-                                " UNION SELECT 'CREATE'"
-                                " UNION SELECT 'CONNECT'"
-                                " UNION SELECT 'TEMPORARY'"
-                                " UNION SELECT 'EXECUTE'"
-                                " UNION SELECT 'USAGE'"
-                                " UNION SELECT 'ALL'");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                     "SELECT",
+                                     "INSERT",
+                                     "UPDATE",
+                                     "DELETE",
+                                     "TRUNCATE",
+                                     "REFERENCES",
+                                     "TRIGGER",
+                                     "CREATE",
+                                     "CONNECT",
+                                     "TEMPORARY",
+                                     "EXECUTE",
+                                     "USAGE",
+                                     "ALL");
     }

     /*
@@ -3607,9 +3695,6 @@ psql_completion(const char *text, int start, int end)
     /*
      * Complete GRANT/REVOKE <sth> ON with a list of appropriate relations.
      *
-     * Keywords like DATABASE, FUNCTION, LANGUAGE and SCHEMA added to query
-     * result via UNION; seems to work intuitively.
-     *
      * Note: GRANT/REVOKE can get quite complex; tab-completion as implemented
      * here will only work if the privilege list contains exactly one
      * privilege.
@@ -3623,26 +3708,26 @@ psql_completion(const char *text, int start, int end)
         if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
             COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
         else
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables,
-                                       " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
-                                       " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
-                                       " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
-                                       " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
-                                       " UNION SELECT 'ALL TABLES IN SCHEMA'"
-                                       " UNION SELECT 'DATABASE'"
-                                       " UNION SELECT 'DOMAIN'"
-                                       " UNION SELECT 'FOREIGN DATA WRAPPER'"
-                                       " UNION SELECT 'FOREIGN SERVER'"
-                                       " UNION SELECT 'FUNCTION'"
-                                       " UNION SELECT 'LANGUAGE'"
-                                       " UNION SELECT 'LARGE OBJECT'"
-                                       " UNION SELECT 'PROCEDURE'"
-                                       " UNION SELECT 'ROUTINE'"
-                                       " UNION SELECT 'SCHEMA'"
-                                       " UNION SELECT 'SEQUENCE'"
-                                       " UNION SELECT 'TABLE'"
-                                       " UNION SELECT 'TABLESPACE'"
-                                       " UNION SELECT 'TYPE'");
+            COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables,
+                                            "ALL FUNCTIONS IN SCHEMA",
+                                            "ALL PROCEDURES IN SCHEMA",
+                                            "ALL ROUTINES IN SCHEMA",
+                                            "ALL SEQUENCES IN SCHEMA",
+                                            "ALL TABLES IN SCHEMA",
+                                            "DATABASE",
+                                            "DOMAIN",
+                                            "FOREIGN DATA WRAPPER",
+                                            "FOREIGN SERVER",
+                                            "FUNCTION",
+                                            "LANGUAGE",
+                                            "LARGE OBJECT",
+                                            "PROCEDURE",
+                                            "ROUTINE",
+                                            "SCHEMA",
+                                            "SEQUENCE",
+                                            "TABLE",
+                                            "TABLESPACE",
+                                            "TYPE");
     }
     else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL"))
         COMPLETE_WITH("FUNCTIONS IN SCHEMA",
@@ -3664,25 +3749,25 @@ psql_completion(const char *text, int start, int end)
         if (TailMatches("DATABASE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_databases);
         else if (TailMatches("DOMAIN"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
         else if (TailMatches("FUNCTION"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
         else if (TailMatches("LANGUAGE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_languages);
         else if (TailMatches("PROCEDURE"))
-            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+            COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures);
         else if (TailMatches("ROUTINE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
         else if (TailMatches("SCHEMA"))
             COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
         else if (TailMatches("SEQUENCE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
         else if (TailMatches("TABLE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
         else if (TailMatches("TABLESPACE"))
             COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
         else if (TailMatches("TYPE"))
-            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+            COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
         else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny))
             COMPLETE_WITH("TO");
         else
@@ -3695,10 +3780,12 @@ psql_completion(const char *text, int start, int end)
      */
     else if ((HeadMatches("GRANT") && TailMatches("TO")) ||
              (HeadMatches("REVOKE") && TailMatches("FROM")))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "ALTER DEFAULT PRIVILEGES ... GRANT/REVOKE ... TO/FROM */
     else if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES") && TailMatches("TO|FROM"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 Keywords_for_list_of_grant_roles);
     /* Complete "GRANT/REVOKE ... ON * *" with TO/FROM */
     else if (HeadMatches("GRANT") && TailMatches("ON", MatchAny, MatchAny))
         COMPLETE_WITH("TO");
@@ -3759,10 +3846,10 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("INTO");
     /* Complete INSERT INTO with table names */
     else if (TailMatches("INSERT", "INTO"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete "INSERT INTO <table> (" with attribute names */
     else if (TailMatches("INSERT", "INTO", MatchAny, "("))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

     /*
      * Complete INSERT INTO <table> with "(" or "VALUES" or "SELECT" or
@@ -3794,14 +3881,13 @@ psql_completion(const char *text, int start, int end)
 /* LOCK */
     /* Complete LOCK [TABLE] [ONLY] with a list of tables */
     else if (Matches("LOCK"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "TABLE", "ONLY");
     else if (Matches("LOCK", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables,
+                                        "ONLY");
     else if (Matches("LOCK", "TABLE", "ONLY") || Matches("LOCK", "ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     /* For the following, handle the case of a single table only for now */

     /* Complete LOCK [TABLE] [ONLY] <table> with IN or NOWAIT */
@@ -3837,7 +3923,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s'"); 
+        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -3851,7 +3937,7 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatches("FROM", MatchAny, "ORDER"))
         COMPLETE_WITH("BY");
     else if (TailMatches("FROM", MatchAny, "ORDER", "BY"))
-        COMPLETE_WITH_ATTR(prev3_wd, "");
+        COMPLETE_WITH_ATTR(prev3_wd);

 /* PREPARE xx AS */
     else if (Matches("PREPARE", MatchAny, "AS"))
@@ -3880,10 +3966,10 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("REFRESH", "MATERIALIZED"))
         COMPLETE_WITH("VIEW");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_matviews,
+                                        "CONCURRENTLY");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", MatchAny))
         COMPLETE_WITH("WITH");
     else if (Matches("REFRESH", "MATERIALIZED", "VIEW", "CONCURRENTLY", MatchAny))
@@ -3903,26 +3989,26 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("TABLE", "INDEX", "SYSTEM", "SCHEMA", "DATABASE");
     else if (Matches("REINDEX", "TABLE") ||
              Matches("REINDEX", "(*)", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexables,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "INDEX") ||
              Matches("REINDEX", "(*)", "INDEX"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes,
-                                   " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_indexes,
+                                        "CONCURRENTLY");
     else if (Matches("REINDEX", "SCHEMA") ||
              Matches("REINDEX", "(*)", "SCHEMA"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "SYSTEM|DATABASE") ||
              Matches("REINDEX", "(*)", "SYSTEM|DATABASE"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_databases
-                            " UNION SELECT 'CONCURRENTLY'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_databases,
+                                 "CONCURRENTLY");
     else if (Matches("REINDEX", "TABLE", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "TABLE", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexables);
     else if (Matches("REINDEX", "INDEX", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "INDEX", "CONCURRENTLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (Matches("REINDEX", "SCHEMA", "CONCURRENTLY") ||
              Matches("REINDEX", "(*)", "SCHEMA", "CONCURRENTLY"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
@@ -3966,9 +4052,17 @@ psql_completion(const char *text, int start, int end)
 /* SET, RESET, SHOW */
     /* Complete with a variable name */
     else if (TailMatches("SET|RESET") && !TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_set_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_set_vars,
+                                 "constraints",
+                                 "transaction",
+                                 "session",
+                                 "role",
+                                 "tablespace",
+                                 "all");
     else if (Matches("SHOW"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_show_vars);
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_show_vars,
+                                 "session authorization",
+                                 "all");
     /* Complete "SET TRANSACTION" */
     else if (Matches("SET", "TRANSACTION"))
         COMPLETE_WITH("SNAPSHOT", "ISOLATION LEVEL", "READ", "DEFERRABLE", "NOT DEFERRABLE");
@@ -4003,7 +4097,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ONLY", "WRITE");
     /* SET CONSTRAINTS */
     else if (Matches("SET", "CONSTRAINTS"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema, "UNION SELECT 'ALL'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_constraints_with_schema,
+                                        "ALL");
     /* Complete SET CONSTRAINTS <foo> with DEFERRED|IMMEDIATE */
     else if (Matches("SET", "CONSTRAINTS", MatchAny))
         COMPLETE_WITH("DEFERRED", "IMMEDIATE");
@@ -4015,7 +4110,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("AUTHORIZATION", "CHARACTERISTICS AS TRANSACTION");
     /* Complete SET SESSION AUTHORIZATION with username */
     else if (Matches("SET", "SESSION", "AUTHORIZATION"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles " UNION SELECT 'DEFAULT'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "DEFAULT");
     /* Complete RESET SESSION with AUTHORIZATION */
     else if (Matches("RESET", "SESSION"))
         COMPLETE_WITH("AUTHORIZATION");
@@ -4045,10 +4141,10 @@ psql_completion(const char *text, int start, int end)
                           "US", "European", "NonEuropean",
                           "DEFAULT");
         else if (TailMatches("search_path", "TO|="))
-            COMPLETE_WITH_QUERY(Query_for_list_of_schemas
-                                " AND nspname not like 'pg\\_toast%%' "
-                                " AND nspname not like 'pg\\_temp%%' "
-                                " UNION SELECT 'DEFAULT' ");
+            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
+                                     " AND nspname not like 'pg\\_toast%%' "
+                                     " AND nspname not like 'pg\\_temp%%' ",
+                                     "DEFAULT");
         else
         {
             /* generic, type based, GUC support */
@@ -4067,7 +4163,7 @@ psql_completion(const char *text, int start, int end)

                     snprintf(querybuf, sizeof(querybuf),
                              Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY(querybuf);
+                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4086,7 +4182,7 @@ psql_completion(const char *text, int start, int end)

 /* TABLE, but not TABLE embedded in other commands */
     else if (Matches("TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* TABLESAMPLE */
     else if (TailMatches("TABLESAMPLE"))
@@ -4096,14 +4192,13 @@ psql_completion(const char *text, int start, int end)

 /* TRUNCATE */
     else if (Matches("TRUNCATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'TABLE'"
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "TABLE", "ONLY");
     else if (Matches("TRUNCATE", "TABLE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables,
-                                   " UNION SELECT 'ONLY'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_truncatables,
+                                        "ONLY");
     else if (HeadMatches("TRUNCATE") && TailMatches("ONLY"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_truncatables);
     else if (Matches("TRUNCATE", MatchAny) ||
              Matches("TRUNCATE", "TABLE|ONLY", MatchAny) ||
              Matches("TRUNCATE", "TABLE", "ONLY", MatchAny))
@@ -4113,18 +4208,20 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY("SELECT pg_catalog.quote_ident(channel) FROM pg_catalog.pg_listening_channels() AS channel
WHEREsubstring(pg_catalog.quote_ident(channel),1,%d)='%s' UNION SELECT '*'"); 
+        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
+                                 " WHERE substring(channel,1,%d)='%s'",
+                                 "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
     else if (TailMatches("UPDATE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_updatables);
     /* Complete UPDATE <table> with "SET" */
     else if (TailMatches("UPDATE", MatchAny))
         COMPLETE_WITH("SET");
     /* Complete UPDATE <table> SET with list of attributes */
     else if (TailMatches("UPDATE", MatchAny, "SET"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     /* UPDATE <table> SET <attr> = */
     else if (TailMatches("UPDATE", MatchAny, "SET", MatchAnyExcept("*=")))
         COMPLETE_WITH("=");
@@ -4133,11 +4230,11 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER|CREATE|DROP", "USER", "MAPPING"))
         COMPLETE_WITH("FOR");
     else if (Matches("CREATE", "USER", "MAPPING", "FOR"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_roles
-                            " UNION SELECT 'CURRENT_ROLE'"
-                            " UNION SELECT 'CURRENT_USER'"
-                            " UNION SELECT 'PUBLIC'"
-                            " UNION SELECT 'USER'");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
+                                 "CURRENT_ROLE",
+                                 "CURRENT_USER",
+                                 "PUBLIC",
+                                 "USER");
     else if (Matches("ALTER|DROP", "USER", "MAPPING", "FOR"))
         COMPLETE_WITH_QUERY(Query_for_list_of_user_mappings);
     else if (Matches("CREATE|ALTER|DROP", "USER", "MAPPING", "FOR", MatchAny))
@@ -4150,26 +4247,26 @@ psql_completion(const char *text, int start, int end)
  * VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ]
  */
     else if (Matches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FULL'"
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FULL",
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FULL"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'FREEZE'"
-                                   " UNION SELECT 'ANALYZE'"
-                                   " UNION SELECT 'VERBOSE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "FREEZE",
+                                        "ANALYZE",
+                                        "VERBOSE");
     else if (Matches("VACUUM", "FREEZE") ||
              Matches("VACUUM", "FULL", "FREEZE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'VERBOSE'"
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "VERBOSE",
+                                        "ANALYZE");
     else if (Matches("VACUUM", "VERBOSE") ||
              Matches("VACUUM", "FULL|FREEZE", "VERBOSE") ||
              Matches("VACUUM", "FULL", "FREEZE", "VERBOSE"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables,
-                                   " UNION SELECT 'ANALYZE'");
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables,
+                                        "ANALYZE");
     else if (HeadMatches("VACUUM", "(*") &&
              !HeadMatches("VACUUM", "(*)"))
     {
@@ -4190,9 +4287,9 @@ psql_completion(const char *text, int start, int end)
     }
     else if (HeadMatches("VACUUM") && TailMatches("("))
         /* "VACUUM (" should be caught above, so assume we want columns */
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);
     else if (HeadMatches("VACUUM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables);

 /* WITH [RECURSIVE] */

@@ -4206,16 +4303,16 @@ psql_completion(const char *text, int start, int end)
 /* WHERE */
     /* Simple case of the word before the where being the table name */
     else if (TailMatches(MatchAny, "WHERE"))
-        COMPLETE_WITH_ATTR(prev2_wd, "");
+        COMPLETE_WITH_ATTR(prev2_wd);

 /* ... FROM ... */
 /* TODO: also include SRF ? */
     else if (TailMatches("FROM") && !Matches("COPY|\\copy", MatchAny, "FROM"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* ... JOIN ... */
     else if (TailMatches("JOIN"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables);

 /* Backslash commands */
 /* TODO:  \dc \dd \dl */
@@ -4232,19 +4329,19 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     }
     else if (TailMatchesCS("\\da*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates);
     else if (TailMatchesCS("\\dAc*", MatchAny) ||
              TailMatchesCS("\\dAf*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dAo*", MatchAny) ||
              TailMatchesCS("\\dAp*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_operator_families);
     else if (TailMatchesCS("\\dA*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
     else if (TailMatchesCS("\\db*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
     else if (TailMatchesCS("\\dD*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
     else if (TailMatchesCS("\\des*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_servers);
     else if (TailMatchesCS("\\deu*"))
@@ -4252,9 +4349,9 @@ psql_completion(const char *text, int start, int end)
     else if (TailMatchesCS("\\dew*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_fdws);
     else if (TailMatchesCS("\\df*"))
-        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
+        COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions);
     else if (HeadMatchesCS("\\df*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);

     else if (TailMatchesCS("\\dFd*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_dictionaries);
@@ -4267,51 +4364,51 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_QUERY(Query_for_list_of_ts_configurations);

     else if (TailMatchesCS("\\di*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
     else if (TailMatchesCS("\\dL*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     else if (TailMatchesCS("\\dn*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
     /* no support for completing operators, but we can complete types: */
     else if (HeadMatchesCS("\\do*", MatchAny))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\dp") || TailMatchesCS("\\z"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_grantables);
     else if (TailMatchesCS("\\dPi*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_indexes);
     else if (TailMatchesCS("\\dPt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables);
     else if (TailMatchesCS("\\dP*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_relations);
     else if (TailMatchesCS("\\ds*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences);
     else if (TailMatchesCS("\\dt*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
     else if (TailMatchesCS("\\dT*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
     else if (TailMatchesCS("\\du*") || TailMatchesCS("\\dg*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_roles);
     else if (TailMatchesCS("\\dv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\dx*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_extensions);
     else if (TailMatchesCS("\\dX*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_statistics);
     else if (TailMatchesCS("\\dm*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews);
     else if (TailMatchesCS("\\dE*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_foreign_tables);
     else if (TailMatchesCS("\\dy*"))
         COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers);

     /* must be at end of \d alternatives: */
     else if (TailMatchesCS("\\d*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_relations);

     else if (TailMatchesCS("\\ef"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\ev"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
         COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
@@ -4413,9 +4510,9 @@ psql_completion(const char *text, int start, int end)
             COMPLETE_WITH_CS("default", "verbose", "terse", "sqlstate");
     }
     else if (TailMatchesCS("\\sf*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
     else if (TailMatchesCS("\\sv*"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
     else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|"
                            "\\ir|\\include_relative|\\o|\\out|"
                            "\\s|\\w|\\write|\\lo_import"))
@@ -4432,19 +4529,21 @@ psql_completion(const char *text, int start, int end)
      */
     else
     {
-        int            i;
+        const pgsql_thing_t *wac;

-        for (i = 0; words_after_create[i].name; i++)
+        for (wac = words_after_create; wac->name != NULL; wac++)
         {
-            if (pg_strcasecmp(prev_wd, words_after_create[i].name) == 0)
+            if (pg_strcasecmp(prev_wd, wac->name) == 0)
             {
-                if (words_after_create[i].query)
-                    COMPLETE_WITH_QUERY(words_after_create[i].query);
-                else if (words_after_create[i].vquery)
-                    COMPLETE_WITH_VERSIONED_QUERY(words_after_create[i].vquery);
-                else if (words_after_create[i].squery)
-                    COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(words_after_create[i].squery,
-                                                         NULL);
+                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;
             }
         }
@@ -4557,7 +4656,8 @@ static char *
 complete_from_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, NULL, text, state);
+    return _complete_from_query(completion_charp, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
@@ -4572,22 +4672,22 @@ complete_from_versioned_query(const char *text, int state)
     if (vquery->query == NULL)
         return NULL;

-    return _complete_from_query(vquery->query, NULL, text, state);
+    return _complete_from_query(vquery->query, NULL, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
 complete_from_schema_query(const char *text, int state)
 {
     /* query is assumed to work for any server version */
-    return _complete_from_query(completion_charp, completion_squery,
-                                text, state);
+    return _complete_from_query(NULL, completion_squery, completion_charpp,
+                                completion_verbatim, text, state);
 }

 static char *
 complete_from_versioned_schema_query(const char *text, int state)
 {
     const SchemaQuery *squery = completion_squery;
-    const VersionedQuery *vquery = completion_vquery;

     /* Find appropriate array element */
     while (pset.sversion < squery->min_server_version)
@@ -4596,17 +4696,8 @@ complete_from_versioned_schema_query(const char *text, int state)
     if (squery->catname == NULL)
         return NULL;

-    /* Likewise for the add-on text, if any */
-    if (vquery)
-    {
-        while (pset.sversion < vquery->min_server_version)
-            vquery++;
-        if (vquery->query == NULL)
-            return NULL;
-    }
-
-    return _complete_from_query(vquery ? vquery->query : NULL,
-                                squery, text, state);
+    return _complete_from_query(NULL, squery, completion_charpp,
+                                completion_verbatim, text, state);
 }


@@ -4617,35 +4708,54 @@ complete_from_versioned_schema_query(const char *text, int state)
  *
  * The query can be one of two kinds:
  *
- * 1. A simple query which must contain a %d and a %s, which will be replaced
- * by the string length of the text and the text itself. The query may also
- * have up to four more %s in it; the first two such will be replaced by the
- * value of completion_info_charp, the next two by the value of
- * completion_info_charp2.
+ * 1. A simple query, which must contain a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
+ * The query may also have up to four more %s in it; the first two such will
+ * be replaced by the value of completion_info_charp, the next two by the
+ * value of completion_info_charp2.  (These strings will be escaped to
+ * become SQL literals, so what is actually in the query should be '%s'.)
+ * Simple queries should return a single column of matches.  If "verbatim"
+ * is true, the matches are returned as-is; otherwise, they are taken to
+ * be SQL identifiers and quoted if necessary.
  *
  * 2. A schema query used for completion of both schema and relation names.
- * These are more complex and must contain in the following order:
- * %d %s %d %s %d %s %s %d %s
- * where %d is the string length of the text and %s the text itself.
+ * This is represented by a SchemaQuery object; see that typedef for details.
+ *
+ * See top of file for examples of both kinds of query.
  *
- * If both simple_query and schema_query are non-NULL, then we construct
- * a schema query and append the (uninterpreted) string simple_query to it.
+ * In addition to the query itself, we accept a null-terminated array of
+ * literal keywords, which will be returned if they match the input-so-far
+ * (case insensitively).  (These are in addition to keywords specified
+ * within the schema_query, if any.)
  *
- * It is assumed that strings should be escaped to become SQL literals
- * (that is, what is in the query is actually ... '%s' ...)
+ * If "verbatim" is true, then we use the given text as-is to match the
+ * query results; otherwise we parse it as a possibly-qualified identifier,
+ * and reconstruct suitable quoting afterward.
  *
- * See top of file for examples of both kinds of query.
+ * "text" and "state" are supplied by Readline.  "text" is the word we are
+ * trying to complete.  "state" is zero on first call, nonzero later.
  *
- * "text" and "state" are supplied by readline.
+ * readline will call this repeatedly with the same text and varying
+ * state.  On each call, we are supposed to return a malloc'd string
+ * that is a candidate completion.  Return NULL when done.
  */
 static char *
 _complete_from_query(const char *simple_query,
                      const SchemaQuery *schema_query,
+                     const char *const *keywords,
+                     bool verbatim,
                      const char *text, int state)
 {
     static int    list_index,
-                byte_length;
+                num_schema_only,
+                num_other;
     static PGresult *result = NULL;
+    static bool non_empty_object;
+    static bool schemaquoted;
+    static bool objectquoted;

     /*
      * If this is the first time for this completion, we fetch a list of our
@@ -4654,31 +4764,53 @@ _complete_from_query(const char *simple_query,
     if (state == 0)
     {
         PQExpBufferData query_buffer;
-        char       *e_text;
+        char       *schemaname;
+        char       *objectname;
+        int            object_length = 0;
+        char       *e_schemaname;
+        char       *e_objectname;
         char       *e_info_charp;
         char       *e_info_charp2;
-        const char *pstr = text;
-        int            char_length = 0;

+        /* Reset static state, ensuring no memory leaks */
         list_index = 0;
-        byte_length = strlen(text);
+        num_schema_only = 0;
+        num_other = 0;
+        PQclear(result);
+        result = NULL;
+
+        /* Parse text, splitting into schema and object name if needed */
+        if (verbatim)
+        {
+            objectname = pg_strdup(text);
+            schemaname = NULL;
+        }
+        else
+        {
+            parse_identifier(text,
+                             &schemaname, &objectname,
+                             &schemaquoted, &objectquoted);
+        }
+
+        /* Remember whether the user has typed anything in the object part */
+        non_empty_object = (*objectname != '\0');

         /*
          * Count length as number of characters (not bytes), for passing to
          * substring
          */
-        while (*pstr)
-        {
-            char_length++;
-            pstr += PQmblenBounded(pstr, pset.encoding);
-        }
-
-        /* Free any prior result */
-        PQclear(result);
-        result = NULL;
+        for (const char *p = objectname;
+             *p;
+             p += PQmblenBounded(p, pset.encoding))
+            object_length++;

         /* Set up suitably-escaped copies of textual inputs */
-        e_text = escape_string(text);
+        if (schemaname)
+            e_schemaname = escape_string(schemaname);
+        else
+            e_schemaname = NULL;
+
+        e_objectname = escape_string(objectname);

         if (completion_info_charp)
             e_info_charp = escape_string(completion_info_charp);
@@ -4694,14 +4826,17 @@ _complete_from_query(const char *simple_query,

         if (schema_query)
         {
-            /* schema_query gives us the pieces to assemble */
-            const char *qualresult = schema_query->qualresult;
-
-            if (qualresult == NULL)
-                qualresult = schema_query->result;
+            Assert(simple_query == NULL);

+            /*
+             * We issue different queries depending on whether the input is
+             * already qualified or not.  schema_query gives us the pieces to
+             * assemble.
+             */
+            if (schemaname == NULL)
+            {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s FROM %s WHERE ",
+            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
                               schema_query->result,
                               schema_query->catname);
             if (schema_query->selcondition)
@@ -4709,7 +4844,7 @@ _complete_from_query(const char *simple_query,
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
-                              char_length, e_text);
+                              object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);

@@ -4721,72 +4856,59 @@ _complete_from_query(const char *simple_query,
              */
             if (strcmp(schema_query->catname,
                        "pg_catalog.pg_class c") == 0 &&
-                strncmp(text, "pg_", 3) != 0)
+                strncmp(objectname, "pg_", 3) != 0)
             {
                 appendPQExpBufferStr(&query_buffer,
                                      " AND c.relnamespace <> (SELECT oid FROM"
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

+            /* Add in schema names matching the input-so-far */
+            appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
+                              "SELECT NULL::pg_catalog.text, n.nspname "
+                              "FROM pg_catalog.pg_namespace n "
+                              "WHERE substring(n.nspname,1,%d)='%s'",
+                              object_length, e_objectname);
+
             /*
-             * Add in matching schema names, but only if there is more than
-             * one potential match among schema names.
+             * Likewise, suppress system schemas unless the input-so-far
+             * begins with "pg_".
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' "
-                              "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d)='%s'",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) > 1",
-                              char_length, e_text);
+            if (strncmp(objectname, "pg_", 3) != 0)
+                appendPQExpBufferStr(&query_buffer,
+                                     " AND n.nspname NOT LIKE 'pg\\_%'");

             /*
-             * Add in matching qualified names, but only if there is exactly
-             * one schema matching the input-so-far.
+             * Since we're matching these schema names to the object name,
+             * handle their quoting using the object name's quoting state.
              */
-            appendPQExpBuffer(&query_buffer, "\nUNION\n"
-                              "SELECT pg_catalog.quote_ident(n.nspname) || '.' || %s "
+            schemaquoted = objectquoted;
+            }
+            else
+            {
+            /* Input is qualified, so produce only qualified names */
+            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
                               "FROM %s, pg_catalog.pg_namespace n "
                               "WHERE %s = n.oid AND ",
-                              qualresult,
+                              schema_query->result,
                               schema_query->catname,
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(pg_catalog.quote_ident(n.nspname) || '.' || %s,1,%d)='%s'",
-                              qualresult,
-                              char_length, e_text);
-
-            /*
-             * This condition exploits the single-matching-schema rule to
-             * speed up the query
-             */
-            appendPQExpBuffer(&query_buffer,
-                              " AND substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(n.nspname))+1)",
-                              char_length, e_text);
-            appendPQExpBuffer(&query_buffer,
-                              " AND (SELECT pg_catalog.count(*)"
-                              " FROM pg_catalog.pg_namespace"
-                              " WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) ="
-                              " substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1",
-                              char_length, e_text);
-
-            /* If an addon query was provided, use it */
-            if (simple_query)
-                appendPQExpBuffer(&query_buffer, "\n%s", simple_query);
+            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                              schema_query->result,
+                              object_length, e_objectname);
+            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                              e_schemaname);
+            }
         }
         else
         {
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              char_length, e_text,
+                              object_length, e_objectname,
                               e_info_charp, e_info_charp,
                               e_info_charp2, e_info_charp2);
         }
@@ -4795,28 +4917,120 @@ _complete_from_query(const char *simple_query,
         appendPQExpBuffer(&query_buffer, "\nLIMIT %d",
                           completion_max_records);

+        /* Finally, we can issue the query */
         result = exec_query(query_buffer.data);

+        /* Clean up */
         termPQExpBuffer(&query_buffer);
-        free(e_text);
+        if (e_schemaname)
+            free(e_schemaname);
+        free(e_objectname);
         if (e_info_charp)
             free(e_info_charp);
         if (e_info_charp2)
             free(e_info_charp2);
     }

-    /* Find something that matches */
+    /* Return the next result, if any, but not if the query failed */
     if (result && PQresultStatus(result) == PGRES_TUPLES_OK)
     {
-        const char *item;
+        int            nskip;

-        while (list_index < PQntuples(result) &&
-               (item = PQgetvalue(result, list_index++, 0)))
-            if (pg_strncasecmp(text, item, byte_length) == 0)
+        while (list_index < PQntuples(result))
+        {
+            const char *item = NULL;
+            const char *nsp = NULL;
+
+            if (!PQgetisnull(result, list_index, 0))
+                item = PQgetvalue(result, list_index, 0);
+            if (PQnfields(result) > 1 &&
+                !PQgetisnull(result, list_index, 1))
+                nsp = PQgetvalue(result, list_index, 1);
+            list_index++;
+
+            /* In verbatim mode, we return all the items as-is */
+            if (verbatim)
                 return pg_strdup(item);
+
+            /*
+             * In normal mode, a name requiring quoting will be returned only
+             * if the input was empty or quoted.  Otherwise the user might see
+             * completion inserting a quote she didn't type, which is
+             * surprising.  This restriction also dodges some odd behaviors of
+             * some versions of readline/libedit.
+             */
+            if (non_empty_object)
+            {
+                if (item && !objectquoted && identifier_needs_quotes(item))
+                    continue;
+                if (nsp && !schemaquoted && identifier_needs_quotes(nsp))
+                    continue;
+            }
+
+            /* Count schema-only results for hack below */
+            if (item == NULL && nsp != NULL)
+                num_schema_only++;
+            else
+                num_other++;
+
+            return requote_identifier(nsp, item, schemaquoted, objectquoted);
+        }
+
+        /*
+         * When the query result is exhausted, check for hard-wired keywords.
+         * These will only be returned if they match the input-so-far,
+         * ignoring case.
+         */
+        nskip = list_index - PQntuples(result);
+        if (schema_query && schema_query->keywords)
+        {
+            const char *const *itemp = schema_query->keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                {
+                    num_other++;
+                    return pg_strdup(item);
+                }
+            }
+        }
+        if (keywords)
+        {
+            const char *const *itemp = keywords;
+
+            while (*itemp)
+            {
+                const char *item = *itemp++;
+
+                if (nskip-- > 0)
+                    continue;
+                list_index++;
+                if (pg_strncasecmp(text, item, strlen(text)) == 0)
+                {
+                    num_other++;
+                    return pg_strdup(item);
+                }
+            }
+        }
     }

-    /* If nothing matches, free the db structure and return null */
+    /*
+     * Hack: if we returned only bare schema names, don't let Readline add a
+     * space afterwards.  Otherwise the schema will stop being part of the
+     * completion subject text, which is not what we want.
+     */
+#ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
+    if (num_schema_only > 0 && num_other == 0)
+        rl_completion_append_character = '\0';
+#endif
+
+    /* No more matches, so free the result structure and return null */
     PQclear(result);
     result = NULL;
     return NULL;
@@ -5150,6 +5364,228 @@ escape_string(const char *text)
 }


+/*
+ * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
+ *
+ * This involves splitting off the schema name if present, de-quoting,
+ * and downcasing any unquoted text.  We are a bit laxer than the backend
+ * in that we allow just portions of a name to be quoted --- that's because
+ * psql metacommands have traditionally behaved that way.
+ *
+ * Outputs are a malloc'd schema name (NULL if none), malloc'd object name,
+ * and booleans telling whether any part of the schema and object name was
+ * double-quoted.
+ */
+static void
+parse_identifier(const char *ident,
+                 char **schemaname, char **objectname,
+                 bool *schemaquoted, bool *objectquoted)
+{
+    size_t        buflen = strlen(ident) + 1;
+    bool        enc_is_single_byte = (pg_encoding_max_length(pset.encoding) == 1);
+    char       *sname;
+    char       *oname;
+    char       *optr;
+    bool        inquotes;
+
+    /* Initialize, making a certainly-large-enough output buffer */
+    sname = NULL;
+    oname = pg_malloc(buflen);
+    *schemaquoted = *objectquoted = false;
+    /* Scan */
+    optr = oname;
+    inquotes = false;
+    while (*ident)
+    {
+        unsigned char ch = (unsigned char) *ident++;
+
+        if (ch == '"')
+        {
+            if (inquotes && *ident == '"')
+            {
+                /* two quote marks within a quoted identifier = emit quote */
+                *optr++ = '"';
+                ident++;
+            }
+            else
+            {
+                inquotes = !inquotes;
+                *objectquoted = true;
+            }
+        }
+        else if (ch == '.' && !inquotes)
+        {
+            /* Found a schema name, transfer it to sname / *schemaquoted */
+            *optr = '\0';
+            free(sname);        /* drop any catalog name */
+            sname = oname;
+            oname = pg_malloc(buflen);
+            optr = oname;
+            *schemaquoted = *objectquoted;
+            *objectquoted = false;
+        }
+        else if (!enc_is_single_byte && IS_HIGHBIT_SET(ch))
+        {
+            /*
+             * Transfer multibyte characters without further processing.  They
+             * wouldn't be affected by our downcasing rule anyway, and this
+             * avoids possibly doing the wrong thing in unsafe client
+             * encodings.
+             */
+            int            chlen = PQmblenBounded(ident - 1, pset.encoding);
+
+            *optr++ = (char) ch;
+            while (--chlen > 0)
+                *optr++ = *ident++;
+        }
+        else
+        {
+            if (!inquotes)
+            {
+                /*
+                 * This downcasing transformation should match the backend's
+                 * downcase_identifier() as best we can.  We do not know the
+                 * backend's locale, though, so it's necessarily approximate.
+                 * We assume that psql is operating in the same locale and
+                 * encoding as the backend.
+                 */
+                if (ch >= 'A' && ch <= 'Z')
+                    ch += 'a' - 'A';
+                else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch))
+                    ch = tolower(ch);
+            }
+            *optr++ = (char) ch;
+        }
+    }
+
+    *optr = '\0';
+    *schemaname = sname;
+    *objectname = oname;
+}
+
+
+/*
+ * requote_identifier - Reconstruct a possibly-schema-qualified SQL identifier.
+ *
+ * Build a malloc'd string containing the identifier, with quoting applied
+ * as necessary.  This is more or less the inverse of parse_identifier;
+ * in particular, if an input component was quoted, we'll quote the output
+ * even when that isn't strictly required.
+ *
+ * Unlike parse_identifier, we handle the case where a schema and no
+ * object name is provided, producing just "schema.".
+ */
+static char *
+requote_identifier(const char *schemaname, const char *objectname,
+                   bool quote_schema, bool quote_object)
+{
+    char       *result;
+    size_t        buflen = 1;        /* count the trailing \0 */
+    char       *ptr;
+
+    /*
+     * We could use PQescapeIdentifier for some of this, but not all, and it
+     * adds more notational cruft than it seems worth.
+     */
+    if (schemaname)
+    {
+        buflen += strlen(schemaname) + 1;    /* +1 for the dot */
+        if (!quote_schema)
+            quote_schema = identifier_needs_quotes(schemaname);
+        if (quote_schema)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = schemaname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    if (objectname)
+    {
+        buflen += strlen(objectname);
+        if (!quote_object)
+            quote_object = identifier_needs_quotes(objectname);
+        if (quote_object)
+        {
+            buflen += 2;        /* account for quote marks */
+            for (const char *p = objectname; *p; p++)
+            {
+                if (*p == '"')
+                    buflen++;
+            }
+        }
+    }
+    result = pg_malloc(buflen);
+    ptr = result;
+    if (schemaname)
+    {
+        if (quote_schema)
+            *ptr++ = '"';
+        for (const char *p = schemaname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_schema)
+            *ptr++ = '"';
+        *ptr++ = '.';
+    }
+    if (objectname)
+    {
+        if (quote_object)
+            *ptr++ = '"';
+        for (const char *p = objectname; *p; p++)
+        {
+            *ptr++ = *p;
+            if (*p == '"')
+                *ptr++ = '"';
+        }
+        if (quote_object)
+            *ptr++ = '"';
+    }
+    *ptr = '\0';
+    return result;
+}
+
+
+/*
+ * Detect whether an identifier must be double-quoted.
+ *
+ * Note we'll quote anything that's not ASCII; the backend's quote_ident()
+ * does the same.  Perhaps this could be relaxed in future.
+ */
+static bool
+identifier_needs_quotes(const char *ident)
+{
+    int            kwnum;
+
+    /* Check syntax. */
+    if (!((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'))
+        return true;
+    if (strspn(ident, "abcdefghijklmnopqrstuvwxyz0123456789_") != strlen(ident))
+        return true;
+
+    /*
+     * Check for keyword.  We quote keywords except for unreserved ones.
+     *
+     * It is possible that our keyword list doesn't quite agree with the
+     * server's, but this should be close enough for tab-completion purposes.
+     *
+     * Note: ScanKeywordLookup() does case-insensitive comparison, but that's
+     * fine, since we already know we have all-lower-case.
+     */
+    kwnum = ScanKeywordLookup(ident, &ScanKeywords);
+
+    if (kwnum >= 0 && ScanKeywordCategories[kwnum] != UNRESERVED_KEYWORD)
+        return true;
+
+    return false;
+}
+
+
 /*
  * Execute a query and report any errors. This should be the preferred way of
  * talking to the database in this file.
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e2026275b0..176f7e74c2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -110,6 +110,12 @@ typedef struct VersionedQuery
  * time.  So we put the components of each query into this struct and
  * assemble them with the common boilerplate in _complete_from_query().
  *
+ * We also use this struct to define queries that use completion_info_object,
+ * which is some object related to the one(s) we want to get the names of
+ * (for example, the table we want the indexes of).  In that usage the
+ * objects we're completing might not have a schema of their own, but the
+ * related object almost always does (passed in completion_info_schema).
+ *
  * As with VersionedQuery, we can use an array of these if the query details
  * must vary across versions.
  */
@@ -123,8 +129,9 @@ typedef struct SchemaQuery
     int            min_server_version;

     /*
-     * Name of catalog or catalogs to be queried, with alias, eg.
-     * "pg_catalog.pg_class c".  Note that "pg_namespace n" will be added.
+     * Name of catalog or catalogs to be queried, with alias(es), eg.
+     * "pg_catalog.pg_class c".  Note that "pg_namespace n" and/or
+     * "pg_namespace ni" will be added automatically when needed.
      */
     const char *catname;

@@ -140,12 +147,15 @@ typedef struct SchemaQuery
     /*
      * Visibility condition --- which rows are visible without schema
      * qualification?  For example, "pg_catalog.pg_table_is_visible(c.oid)".
+     * NULL if not needed.
      */
     const char *viscondition;

     /*
-     * Namespace --- name of field to join to pg_namespace.oid. For example,
-     * "c.relnamespace".
+     * Namespace --- name of field to join to pg_namespace.oid when there is
+     * schema qualification.  For example, "c.relnamespace".  NULL if we don't
+     * want to join to pg_namespace (then any schema part in the input word
+     * will be ignored).
      */
     const char *namespace;

@@ -154,12 +164,43 @@ typedef struct SchemaQuery
      */
     const char *result;

+    /*
+     * In some cases, it's difficult to keep the query from returning the same
+     * object multiple times.  Specify use_distinct to filter out duplicates.
+     */
+    bool        use_distinct;
+
     /*
      * Additional literal strings (usually keywords) to be offered along with
      * the query results.  Provide a NULL-terminated array of constant
      * strings, or NULL if none.
      */
     const char *const *keywords;
+
+    /*
+     * If this query uses completion_info_object/completion_info_schema,
+     * populate the remaining fields, else leave them NULL.  When using this
+     * capability, catname must include the catalog that defines the
+     * completion_info_object, and selcondition must include the join
+     * condition that connects it to the result's catalog.
+     *
+     * infoname is the field that should be equated to completion_info_object,
+     * for example "ci.relname".
+     */
+    const char *infoname;
+
+    /*
+     * Visibility condition to use when completion_info_schema is not set. For
+     * example, "pg_catalog.pg_table_is_visible(ci.oid)".  NULL if not needed.
+     */
+    const char *infoviscondition;
+
+    /*
+     * Name of field to join to pg_namespace.oid when completion_info_schema
+     * is set.  For example, "ci.relnamespace".  NULL if we don't want to
+     * consider completion_info_schema.
+     */
+    const char *infonamespace;
 } SchemaQuery;


@@ -176,10 +217,10 @@ static int    completion_max_records;
 static char completion_last_char;    /* last char of input word */
 static const char *completion_charp;    /* to pass a string */
 static const char *const *completion_charpp;    /* to pass a list of strings */
-static const char *completion_info_charp;    /* to pass a second string */
-static const char *completion_info_charp2;    /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;    /* to pass a SchemaQuery */
+static char *completion_info_object;    /* name of a related object */
+static char *completion_info_schema;    /* schema name of a related object */
 static bool completion_case_sensitive;    /* completion is case sensitive */
 static bool completion_verbatim;    /* completion is verbatim */
 static bool completion_force_quote; /* true to force-quote filenames */
@@ -257,6 +298,14 @@ do { \
     COMPLETE_WITH_SCHEMA_QUERY_LIST(query, list); \
 } while (0)

+#define COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(query) \
+do { \
+    completion_squery = &(query); \
+    completion_charpp = NULL; \
+    completion_verbatim = true; \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
+} while (0)
+
 #define COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(query) \
     COMPLETE_WITH_VERSIONED_SCHEMA_QUERY_LIST(query, NULL)

@@ -312,29 +361,11 @@ do { \

 #define COMPLETE_WITH_ATTR_LIST(relation, list) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_table; \
-\
-    _completion_schema = strtokx(relation, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_table = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                false, false, pset.encoding); \
-    if (_completion_table == NULL) \
-    { \
-        completion_charp = Query_for_list_of_attributes; \
-        completion_info_charp = relation; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_attributes_with_schema; \
-        completion_info_charp = _completion_table; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(relation); \
+    completion_squery = &(Query_for_list_of_attributes); \
     completion_charpp = list; \
     completion_verbatim = false; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_ATTR_PLUS(relation, ...) \
@@ -351,65 +382,24 @@ do { \
  */
 #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); \
-    use_quotes = (text[0] == '\'' || \
-                  start == 0 || rl_line_buffer[start - 1] != '\''); \
-    if (_completion_type == NULL) \
-    { \
-        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; \
-    } \
+    setup_completion_info(type); \
+    if (text[0] == '\'' || \
+        start == 0 || rl_line_buffer[start - 1] != '\'') \
+        completion_squery = &(Query_for_list_of_enum_values_quoted); \
     else \
-    { \
-        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; \
-    } \
+        completion_squery = &(Query_for_list_of_enum_values_unquoted); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 #define COMPLETE_WITH_FUNCTION_ARG(function) \
 do { \
-    char   *_completion_schema; \
-    char   *_completion_function; \
-\
-    _completion_schema = strtokx(function, " \t\n\r", ".", "\"", 0, \
-                                 false, false, pset.encoding); \
-    (void) strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                   false, false, pset.encoding); \
-    _completion_function = strtokx(NULL, " \t\n\r", ".", "\"", 0, \
-                                   false, false, pset.encoding); \
-    if (_completion_function == NULL) \
-    { \
-        completion_charp = Query_for_list_of_arguments; \
-        completion_info_charp = function; \
-    } \
-    else \
-    { \
-        completion_charp = Query_for_list_of_arguments_with_schema; \
-        completion_info_charp = _completion_function; \
-        completion_info_charp2 = _completion_schema; \
-    } \
+    setup_completion_info(function); \
+    completion_squery = &(Query_for_list_of_arguments); \
     completion_charpp = NULL; \
     completion_verbatim = true; \
-    matches = rl_completion_matches(text, complete_from_query); \
+    matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)

 /*
@@ -419,6 +409,51 @@ do { \
  * unnecessary bloat in the completions generated.
  */

+static const SchemaQuery Query_for_constraint_of_table = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_table_not_validated = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_class c1",
+    .selcondition = "con.conrelid=c1.oid and not con.convalidated",
+    .result = "con.conname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_constraint_of_type = {
+    .catname = "pg_catalog.pg_constraint con, pg_catalog.pg_type t",
+    .selcondition = "con.contypid=t.oid",
+    .result = "con.conname",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_unique_index_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i",
+    .selcondition = "c1.oid=i.indrelid and i.indexrelid=c2.oid and i.indisunique",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
 static const SchemaQuery Query_for_list_of_aggregates[] = {
     {
         .min_server_version = 110000,
@@ -437,6 +472,32 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
     }
 };

+static const SchemaQuery Query_for_list_of_arguments = {
+    .catname = "pg_catalog.pg_proc p",
+    .result = "pg_catalog.oidvectortypes(p.proargtypes)||')'",
+    .infoname = "p.proname",
+    .infoviscondition = "pg_catalog.pg_function_is_visible(p.oid)",
+    .infonamespace = "p.pronamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attributes = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attname",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_attribute_numbers = {
+    .catname = "pg_catalog.pg_attribute a, pg_catalog.pg_class c",
+    .selcondition = "c.oid = a.attrelid and a.attnum > 0 and not a.attisdropped",
+    .result = "a.attnum::pg_catalog.text",
+    .infoname = "c.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .infonamespace = "c.relnamespace",
+};
+
 static const char *const Keywords_for_list_of_datatypes[] = {
     "bigint",
     "boolean",
@@ -497,6 +558,24 @@ static const SchemaQuery Query_for_list_of_domains = {
     .result = "t.typname",
 };

+static const SchemaQuery Query_for_list_of_enum_values_quoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "pg_catalog.quote_literal(enumlabel)",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
+static const SchemaQuery Query_for_list_of_enum_values_unquoted = {
+    .catname = "pg_catalog.pg_enum e, pg_catalog.pg_type t",
+    .selcondition = "t.oid = e.enumtypid",
+    .result = "e.enumlabel",
+    .infoname = "t.typname",
+    .infoviscondition = "pg_catalog.pg_type_is_visible(t.oid)",
+    .infonamespace = "t.typnamespace",
+};
+
 /* Note: this intentionally accepts aggregates as well as plain functions */
 static const SchemaQuery Query_for_list_of_functions[] = {
     {
@@ -571,6 +650,48 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
     .result = "c.relname",
 };

+static const SchemaQuery Query_for_list_of_tables_for_constraint = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_constraint con",
+    .selcondition = "c.oid=con.conrelid and c.relkind IN ("
+    CppAsString2(RELKIND_RELATION) ", "
+    CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "con.conname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_policy = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_policy p",
+    .selcondition = "c.oid=p.polrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "p.polname",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_rule = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_rewrite r",
+    .selcondition = "c.oid=r.ev_class",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "r.rulename",
+};
+
+static const SchemaQuery Query_for_list_of_tables_for_trigger = {
+    .catname = "pg_catalog.pg_class c, pg_catalog.pg_trigger t",
+    .selcondition = "c.oid=t.tgrelid",
+    .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+    .namespace = "c.relnamespace",
+    .result = "c.relname",
+    .use_distinct = true,
+    .infoname = "t.tgname",
+};
+
 static const SchemaQuery Query_for_list_of_views = {
     .catname = "pg_catalog.pg_class c",
     .selcondition = "c.relkind IN (" CppAsString2(RELKIND_VIEW) ")",
@@ -719,7 +840,6 @@ static const SchemaQuery Query_for_list_of_clusterables = {
 static const SchemaQuery Query_for_list_of_constraints_with_schema = {
     .catname = "pg_catalog.pg_constraint c",
     .selcondition = "c.conrelid <> 0",
-    .viscondition = "true",        /* there is no pg_constraint_is_visible */
     .namespace = "c.connamespace",
     .result = "c.conname",
 };
@@ -739,96 +859,56 @@ static const SchemaQuery Query_for_list_of_collations = {
     .result = "c.collname",
 };

+static const SchemaQuery Query_for_partition_of_table = {
+    .catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
+    .selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
+    .viscondition = "pg_catalog.pg_table_is_visible(c2.oid)",
+    .namespace = "c2.relnamespace",
+    .result = "c2.relname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_rule_of_table = {
+    .catname = "pg_catalog.pg_rewrite r, pg_catalog.pg_class c1",
+    .selcondition = "r.ev_class=c1.oid",
+    .result = "r.rulename",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+
+static const SchemaQuery Query_for_trigger_of_table = {
+    .catname = "pg_catalog.pg_trigger t, pg_catalog.pg_class c1",
+    .selcondition = "t.tgrelid=c1.oid and not t.tgisinternal",
+    .result = "t.tgname",
+    .infoname = "c1.relname",
+    .infoviscondition = "pg_catalog.pg_table_is_visible(c1.oid)",
+    .infonamespace = "c1.relnamespace",
+};
+

 /*
  * Queries to get lists of names of various kinds of things, possibly
- * restricted to names matching a partially entered name.  In these queries,
- * the first %s will be replaced by the text entered so far (suitably escaped
- * to become a SQL literal string).  %d will be replaced by the length of the
- * string (in unescaped form).  A second and third %s, if present, will be
- * replaced by a suitably-escaped version of the string provided in
- * completion_info_charp.  A fourth and fifth %s are similarly replaced by
- * completion_info_charp2.
+ * restricted to names matching a partially entered name.  Don't use
+ * this method where the user might wish to enter a schema-qualified
+ * name; make a SchemaQuery instead.
+ *
+ * In these queries, there must be a restriction clause of the form
+ *        substring(OUTPUT,1,%d)='%s'
+ * where "OUTPUT" is the same string that the query returns.  The %d and %s
+ * will be replaced by the string length of the text and the text itself,
+ * causing the results to be limited to those matching the already-typed text.
  *
- * Beware that the allowed sequences of %s and %d are determined by
- * _complete_from_query().
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  NOTE: using completion_info_object
+ * that way is usually the wrong thing, and using completion_info_schema
+ * that way is always the wrong thing.  Make a SchemaQuery instead.
  */

-#define Query_for_list_of_attributes \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attribute_numbers \
-"SELECT attnum "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c "\
-" WHERE c.oid = a.attrelid "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attnum::pg_catalog.text,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"'='%s') "\
-"   AND pg_catalog.pg_table_is_visible(c.oid)"
-
-#define Query_for_list_of_attributes_with_schema \
-"SELECT attname "\
-"  FROM pg_catalog.pg_attribute a, pg_catalog.pg_class c, pg_catalog.pg_namespace n "\
-" WHERE c.oid = a.attrelid "\
-"   AND n.oid = c.relnamespace "\
-"   AND a.attnum > 0 "\
-"   AND NOT a.attisdropped "\
-"   AND substring(attname,1,%d)='%s' "\
-"   AND (relname='%s' "\
-"        OR '\"' || relname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
-#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 "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND pg_catalog.pg_type_is_visible(t.oid)"
-
-#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 (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 "\
-"   AND n.oid = t.typnamespace "\
-"   AND substring(pg_catalog.quote_literal(enumlabel),1,%d)='%s' "\
-"   AND (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (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 (typname='%s' "\
-"        OR '\"' || typname || '\"'='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_template_databases \
 "SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
@@ -887,81 +967,12 @@ static const SchemaQuery Query_for_list_of_collations = {
 #define Keywords_for_list_of_grant_roles \
 "PUBLIC", "CURRENT_ROLE", "CURRENT_USER", "SESSION_USER"

-#define Query_for_index_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_index i"\
-" WHERE c1.oid=i.indrelid and i.indexrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"
-
-#define Query_for_unique_index_of_table \
-Query_for_index_of_table \
-"       and i.indisunique"
-
-#define Query_for_constraint_of_table \
-"SELECT conname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_constraint con "\
-" WHERE c1.oid=conrelid and substring(conname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_constraint_of_table_not_validated \
-Query_for_constraint_of_table \
-"       and not con.convalidated"
-
 #define Query_for_all_table_constraints \
 "SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "\
 "       and substring(conname,1,%d)='%s'"

-#define Query_for_constraint_of_type \
-"SELECT conname "\
-"  FROM pg_catalog.pg_type t, pg_catalog.pg_constraint con "\
-" WHERE t.oid=contypid and substring(conname,1,%d)='%s'"\
-"       and t.typname='%s'"\
-"       and pg_catalog.pg_type_is_visible(t.oid)"
-
-#define Query_for_list_of_tables_for_constraint \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT conrelid FROM pg_catalog.pg_constraint "\
-"         WHERE conname='%s')"
-
-#define Query_for_rule_of_table \
-"SELECT rulename "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_rewrite "\
-" WHERE c1.oid=ev_class and substring(rulename,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"
-
-#define Query_for_list_of_tables_for_rule \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT ev_class FROM pg_catalog.pg_rewrite "\
-"         WHERE rulename='%s')"
-
-#define Query_for_trigger_of_table \
-"SELECT tgname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_trigger "\
-" WHERE c1.oid=tgrelid and substring(tgname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c1.oid)"\
-"       and not tgisinternal"
-
-#define Query_for_list_of_tables_for_trigger \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT tgrelid FROM pg_catalog.pg_trigger "\
-"         WHERE tgname='%s')"
-
 #define Query_for_list_of_ts_configurations \
 "SELECT cfgname FROM pg_catalog.pg_ts_config "\
 " WHERE substring(cfgname,1,%d)='%s'"
@@ -1010,24 +1021,6 @@ Query_for_constraint_of_table \
 "  WHERE substring(amname,1,%d)='%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

-#define Query_for_list_of_arguments \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND (proname='%s'"\
-"        OR '\"' || proname || '\"'='%s') "\
-"   AND (pg_catalog.pg_function_is_visible(pg_proc.oid))"
-
-#define Query_for_list_of_arguments_with_schema \
-"SELECT pg_catalog.oidvectortypes(proargtypes)||')' "\
-"  FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n "\
-" WHERE substring(pg_catalog.oidvectortypes(proargtypes)||')',1,%d)='%s'"\
-"   AND n.oid = p.pronamespace "\
-"   AND (proname='%s' "\
-"        OR '\"' || proname || '\"' ='%s') "\
-"   AND (nspname='%s' "\
-"        OR '\"' || nspname || '\"' ='%s') "
-
 #define Query_for_list_of_extensions \
 " SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
@@ -1074,30 +1067,18 @@ Query_for_constraint_of_table \
 "   FROM pg_catalog.pg_policy "\
 "  WHERE substring(polname,1,%d)='%s'"

-#define Query_for_list_of_tables_for_policy \
-"SELECT relname "\
-"  FROM pg_catalog.pg_class"\
-" WHERE substring(relname,1,%d)='%s'"\
-"   AND oid IN "\
-"       (SELECT polrelid FROM pg_catalog.pg_policy "\
-"         WHERE polname='%s')"
-
-#define Query_for_enum \
-" SELECT name FROM ( "\
-"   SELECT pg_catalog.unnest(enumvals) AS name "\
+#define Query_for_values_of_enum_GUC \
+" SELECT val FROM ( "\
+"   SELECT name, pg_catalog.unnest(enumvals) AS val "\
 "     FROM pg_catalog.pg_settings "\
-"    WHERE pg_catalog.lower(name)=pg_catalog.lower('%s') "\
 "    ) ss "\
-"  WHERE pg_catalog.substring(name,1,%%d)='%%s'"
+"  WHERE substring(val,1,%d)='%s'"\
+"        and pg_catalog.lower(name)=pg_catalog.lower('%s')"

-#define Query_for_partition_of_table \
-"SELECT c2.relname "\
-"  FROM pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i"\
-" WHERE c1.oid=i.inhparent and i.inhrelid=c2.oid"\
-"       and substring(c2.relname,1,%d)='%s'"\
-"       and c1.relname='%s'"\
-"       and pg_catalog.pg_table_is_visible(c2.oid)"\
-"       and c2.relispartition = 'true'"
+#define Query_for_list_of_channels \
+" SELECT channel "\
+"   FROM pg_catalog.pg_listening_channels() AS channel "\
+"  WHERE substring(channel,1,%d)='%s'"

 #define Query_for_list_of_cursors \
 " SELECT name "\
@@ -1280,6 +1261,7 @@ static char *_complete_from_query(const char *simple_query,
                                   const char *const *keywords,
                                   bool verbatim,
                                   const char *text, int state);
+static void setup_completion_info(const char *word);
 static char *complete_from_list(const char *text, int state);
 static char *complete_from_const(const char *text, int state);
 static void append_variable_names(char ***varnames, int *nvars,
@@ -1685,8 +1667,10 @@ psql_completion(const char *text, int start, int end)
     /* Clear a few things. */
     completion_charp = NULL;
     completion_charpp = NULL;
-    completion_info_charp = NULL;
-    completion_info_charp2 = NULL;
+    completion_vquery = NULL;
+    completion_squery = NULL;
+    completion_info_object = NULL;
+    completion_info_schema = NULL;

     /*
      * Scan the input line to extract the words before our current position.
@@ -1872,14 +1856,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER EXTENSION <name> UPDATE */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions_with_TO);
     }

     /* ALTER EXTENSION <name> UPDATE TO */
     else if (Matches("ALTER", "EXTENSION", MatchAny, "UPDATE", "TO"))
     {
-        completion_info_charp = prev3_wd;
+        setup_completion_info(prev3_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -1919,8 +1903,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER INDEX <name> ALTER COLUMN */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_attribute_numbers);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_VERBATIM(Query_for_list_of_attribute_numbers);
     }
     /* ALTER INDEX <name> ALTER COLUMN <colnum> */
     else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN", MatchAny))
@@ -2030,8 +2014,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER DOMAIN <sth> DROP|RENAME|VALIDATE CONSTRAINT */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "DROP|RENAME|VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_type);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_type);
     }
     /* ALTER DOMAIN <sth> RENAME */
     else if (Matches("ALTER", "DOMAIN", MatchAny, "RENAME"))
@@ -2116,8 +2100,8 @@ psql_completion(const char *text, int start, int end)
     /* ALTER POLICY <name> ON <table> */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     /* ALTER POLICY <name> ON <table> - show options */
     else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny))
@@ -2140,8 +2124,8 @@ psql_completion(const char *text, int start, int end)
     /* If we have ALTER RULE <name> ON, then add the correct tablename */
     else if (Matches("ALTER", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }

     /* ALTER RULE <name> ON <name> */
@@ -2156,18 +2140,12 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("ALTER", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");

-    else if (Matches("ALTER", "TRIGGER", MatchAny, MatchAny))
+    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }

-    /*
-     * If we have ALTER TRIGGER <sth> ON, then add the correct tablename
-     */
-    else if (Matches("ALTER", "TRIGGER", MatchAny, "ON"))
-        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
-
     /* ALTER TRIGGER <name> ON <name> */
     else if (Matches("ALTER", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("RENAME TO", "DEPENDS ON EXTENSION",
@@ -2206,28 +2184,28 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE xxx ADD PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev6_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev6_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy PRIMARY KEY USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "PRIMARY", "KEY", "USING", "INDEX"))
     {
-        completion_info_charp = prev8_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev8_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ADD CONSTRAINT yyy UNIQUE USING INDEX */
     else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny,
                      "UNIQUE", "USING", "INDEX"))
     {
-        completion_info_charp = prev7_wd;
-        COMPLETE_WITH_QUERY(Query_for_unique_index_of_table);
+        setup_completion_info(prev7_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_unique_index_of_table);
     }
     /* ALTER TABLE xxx ENABLE */
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE"))
@@ -2237,23 +2215,23 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "RULE"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "ENABLE", MatchAny, "TRIGGER"))
     {
-        completion_info_charp = prev4_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev4_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }
     /* ALTER TABLE xxx INHERIT */
     else if (Matches("ALTER", "TABLE", MatchAny, "INHERIT"))
@@ -2269,13 +2247,13 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ROW LEVEL SECURITY", "RULE", "TRIGGER");
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "RULE"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_rule_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_rule_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DISABLE", "TRIGGER"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_trigger_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_trigger_of_table);
     }

     /* ALTER TABLE xxx ALTER */
@@ -2305,14 +2283,14 @@ psql_completion(const char *text, int start, int end)
     /* ALTER TABLE <sth> ALTER|DROP|RENAME CONSTRAINT <constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER|DROP|RENAME", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table);
     }
     /* ALTER TABLE <sth> VALIDATE CONSTRAINT <non-validated constraint> */
     else if (Matches("ALTER", "TABLE", MatchAny, "VALIDATE", "CONSTRAINT"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_constraint_of_table_not_validated);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_constraint_of_table_not_validated);
     }
     /* ALTER TABLE ALTER [COLUMN] <foo> */
     else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny) ||
@@ -2344,8 +2322,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("ALTER", "TABLE", MatchAny, "CLUSTER", "ON"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     /* If we have ALTER TABLE <sth> SET, provide list of attributes and '(' */
     else if (Matches("ALTER", "TABLE", MatchAny, "SET"))
@@ -2376,8 +2354,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_LIST(table_storage_parameters);
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING", "INDEX"))
     {
-        completion_info_charp = prev5_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev5_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "REPLICA", "IDENTITY", "USING"))
         COMPLETE_WITH("INDEX");
@@ -2404,8 +2382,8 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION"))
     {
-        completion_info_charp = prev3_wd;
-        COMPLETE_WITH_QUERY(Query_for_partition_of_table);
+        setup_completion_info(prev3_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table);
     }
     else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny))
         COMPLETE_WITH("CONCURRENTLY", "FINALIZE");
@@ -2542,8 +2520,8 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CLUSTER", MatchAny, "USING") ||
              Matches("CLUSTER", "VERBOSE|(*)", MatchAny, "USING"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_index_of_table);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
     }
     else if (HeadMatches("CLUSTER", "(*") &&
              !HeadMatches("CLUSTER", "(*)"))
@@ -2580,9 +2558,9 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
-                                 "DOMAIN");
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables_for_constraint,
+                                        "DOMAIN");
     }
     else if (Matches("COMMENT", "ON", "CONSTRAINT", MatchAny, "ON", "DOMAIN"))
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
@@ -2600,8 +2578,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("COMMENT", "ON", "PROCEDURAL", "LANGUAGE"))
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
@@ -2609,8 +2587,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("COMMENT", "ON", "TEXT", "SEARCH"))
         COMPLETE_WITH("CONFIGURATION", "DICTIONARY", "PARSER", "TEMPLATE");
@@ -2628,15 +2606,15 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("COMMENT", "ON", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny))
         COMPLETE_WITH("ON");
     else if (Matches("COMMENT", "ON", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("COMMENT", "ON", MatchAny, MatchAnyExcept("IS")) ||
              Matches("COMMENT", "ON", MatchAny, MatchAny, MatchAnyExcept("IS")) ||
@@ -2752,7 +2730,7 @@ psql_completion(const char *text, int start, int end)
     /* CREATE EXTENSION <name> VERSION */
     else if (Matches("CREATE", "EXTENSION", MatchAny, "VERSION"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_available_extension_versions);
     }

@@ -3063,7 +3041,7 @@ psql_completion(const char *text, int start, int end)
     else if (Matches("CREATE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE") ||
              Matches("CREATE", "OR", "REPLACE", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }

@@ -3486,8 +3464,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_trigger);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_trigger);
     }
     else if (Matches("DROP", "TRIGGER", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3513,8 +3491,8 @@ psql_completion(const char *text, int start, int end)
     /* DROP POLICY <name> ON <table> */
     else if (Matches("DROP", "POLICY", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_policy);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_policy);
     }
     else if (Matches("DROP", "POLICY", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3524,8 +3502,8 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("ON");
     else if (Matches("DROP", "RULE", MatchAny, "ON"))
     {
-        completion_info_charp = prev2_wd;
-        COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_rule);
+        setup_completion_info(prev2_wd);
+        COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables_for_rule);
     }
     else if (Matches("DROP", "RULE", MatchAny, "ON", MatchAny))
         COMPLETE_WITH("CASCADE", "RESTRICT");
@@ -3539,7 +3517,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("LANGUAGE");
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE"))
     {
-        completion_info_charp = prev2_wd;
+        setup_completion_info(prev2_wd);
         COMPLETE_WITH_QUERY(Query_for_list_of_languages);
     }
     else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny))
@@ -3923,7 +3901,7 @@ psql_completion(const char *text, int start, int end)

 /* NOTIFY --- can be inside EXPLAIN, RULE, etc */
     else if (TailMatches("NOTIFY"))
-        COMPLETE_WITH_QUERY("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel WHERE
substring(channel,1,%d)='%s'");
+        COMPLETE_WITH_QUERY(Query_for_list_of_channels);

 /* OPTIONS */
     else if (TailMatches("OPTIONS"))
@@ -4159,11 +4137,9 @@ psql_completion(const char *text, int start, int end)
             {
                 if (strcmp(guctype, "enum") == 0)
                 {
-                    char        querybuf[1024];
-
-                    snprintf(querybuf, sizeof(querybuf),
-                             Query_for_enum, prev2_wd);
-                    COMPLETE_WITH_QUERY_PLUS(querybuf, "DEFAULT");
+                    setup_completion_info(prev2_wd);
+                    COMPLETE_WITH_QUERY_PLUS(Query_for_values_of_enum_GUC,
+                                             "DEFAULT");
                 }
                 else if (strcmp(guctype, "bool") == 0)
                     COMPLETE_WITH("on", "off", "true", "false", "yes", "no",
@@ -4208,9 +4184,7 @@ psql_completion(const char *text, int start, int end)

 /* UNLISTEN */
     else if (Matches("UNLISTEN"))
-        COMPLETE_WITH_QUERY_PLUS("SELECT channel FROM pg_catalog.pg_listening_channels() AS channel"
-                                 " WHERE substring(channel,1,%d)='%s'",
-                                 "*");
+        COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_channels, "*");

 /* UPDATE --- can be inside EXPLAIN, RULE, etc */
     /* If prev. word is UPDATE suggest a list of tables */
@@ -4411,7 +4385,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);

     else if (TailMatchesCS("\\encoding"))
-        COMPLETE_WITH_QUERY(Query_for_list_of_encodings);
+        COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_encodings);
     else if (TailMatchesCS("\\h|\\help"))
         COMPLETE_WITH_LIST(sql_commands);
     else if (TailMatchesCS("\\h|\\help", MatchAny))
@@ -4570,6 +4544,12 @@ psql_completion(const char *text, int start, int end)
     free(previous_words);
     free(words_buffer);
     free(text_copy);
+    if (completion_info_object)
+        free(completion_info_object);
+    completion_info_object = NULL;
+    if (completion_info_schema)
+        free(completion_info_schema);
+    completion_info_schema = NULL;

     /* Return our Grand List O' Matches */
     return matches;
@@ -4713,10 +4693,11 @@ complete_from_versioned_schema_query(const char *text, int state)
  * where "OUTPUT" is the same string that the query returns.  The %d and %s
  * will be replaced by the string length of the text and the text itself,
  * causing the results to be limited to those matching the already-typed text.
- * The query may also have up to four more %s in it; the first two such will
- * be replaced by the value of completion_info_charp, the next two by the
- * value of completion_info_charp2.  (These strings will be escaped to
- * become SQL literals, so what is actually in the query should be '%s'.)
+ * There can be a second '%s', which will be replaced by a suitably-escaped
+ * version of the string provided in completion_info_object.  If there is a
+ * third '%s', it will be replaced by a suitably-escaped version of the string
+ * provided in completion_info_schema.  Those strings should be set up
+ * by calling setup_completion_info().
  * Simple queries should return a single column of matches.  If "verbatim"
  * is true, the matches are returned as-is; otherwise, they are taken to
  * be SQL identifiers and quoted if necessary.
@@ -4769,8 +4750,8 @@ _complete_from_query(const char *simple_query,
         int            object_length = 0;
         char       *e_schemaname;
         char       *e_objectname;
-        char       *e_info_charp;
-        char       *e_info_charp2;
+        char       *e_info_object;
+        char       *e_info_schema;

         /* Reset static state, ensuring no memory leaks */
         list_index = 0;
@@ -4812,15 +4793,15 @@ _complete_from_query(const char *simple_query,

         e_objectname = escape_string(objectname);

-        if (completion_info_charp)
-            e_info_charp = escape_string(completion_info_charp);
+        if (completion_info_object)
+            e_info_object = escape_string(completion_info_object);
         else
-            e_info_charp = NULL;
+            e_info_object = NULL;

-        if (completion_info_charp2)
-            e_info_charp2 = escape_string(completion_info_charp2);
+        if (completion_info_schema)
+            e_info_schema = escape_string(completion_info_schema);
         else
-            e_info_charp2 = NULL;
+            e_info_schema = NULL;

         initPQExpBuffer(&query_buffer);

@@ -4833,20 +4814,44 @@ _complete_from_query(const char *simple_query,
              * already qualified or not.  schema_query gives us the pieces to
              * assemble.
              */
-            if (schemaname == NULL)
+            if (schemaname == NULL || schema_query->namespace == NULL)
             {
             /* Get unqualified names matching the input-so-far */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, NULL::pg_catalog.text FROM %s WHERE ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer,
+                              "%s, NULL::pg_catalog.text FROM %s",
                               schema_query->result,
                               schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBufferStr(&query_buffer, " WHERE ");
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
                                   schema_query->selcondition);
             appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
                               schema_query->result,
                               object_length, e_objectname);
+            if (schema_query->viscondition)
             appendPQExpBuffer(&query_buffer, " AND %s",
                               schema_query->viscondition);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }

             /*
              * When fetching relation names, suppress system catalogs unless
@@ -4863,7 +4868,12 @@ _complete_from_query(const char *simple_query,
                                      " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
             }

-            /* Add in schema names matching the input-so-far */
+            /*
+             * If the target object type can be schema-qualified, add in
+             * schema names matching the input-so-far.
+             */
+            if (schema_query->namespace)
+            {
             appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
                               "SELECT NULL::pg_catalog.text, n.nspname "
                               "FROM pg_catalog.pg_namespace n "
@@ -4884,14 +4894,21 @@ _complete_from_query(const char *simple_query,
              */
             schemaquoted = objectquoted;
             }
+            }
             else
             {
             /* Input is qualified, so produce only qualified names */
-            appendPQExpBuffer(&query_buffer, "SELECT %s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n "
-                              "WHERE %s = n.oid AND ",
+            appendPQExpBufferStr(&query_buffer, "SELECT ");
+            if (schema_query->use_distinct)
+                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                              "FROM %s, pg_catalog.pg_namespace n",
                               schema_query->result,
-                              schema_query->catname,
+                              schema_query->catname);
+            if (schema_query->infonamespace && completion_info_schema)
+                appendPQExpBufferStr(&query_buffer,
+                                     ", pg_catalog.pg_namespace ni");
+            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
                               schema_query->namespace);
             if (schema_query->selcondition)
                 appendPQExpBuffer(&query_buffer, "%s AND ",
@@ -4901,6 +4918,21 @@ _complete_from_query(const char *simple_query,
                               object_length, e_objectname);
             appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
                               e_schemaname);
+            if (schema_query->infoname)
+            {
+                Assert(completion_info_object);
+                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                  schema_query->infoname, e_info_object);
+                if (schema_query->infonamespace && completion_info_schema)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s = ni.oid AND ni.nspname = '%s'",
+                                      schema_query->infonamespace,
+                                      e_info_schema);
+                else if (schema_query->infoviscondition)
+                    appendPQExpBuffer(&query_buffer,
+                                      " AND %s",
+                                      schema_query->infoviscondition);
+            }
             }
         }
         else
@@ -4909,8 +4941,7 @@ _complete_from_query(const char *simple_query,
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
                               object_length, e_objectname,
-                              e_info_charp, e_info_charp,
-                              e_info_charp2, e_info_charp2);
+                              e_info_object, e_info_schema);
         }

         /* Limit the number of records in the result */
@@ -4925,10 +4956,10 @@ _complete_from_query(const char *simple_query,
         if (e_schemaname)
             free(e_schemaname);
         free(e_objectname);
-        if (e_info_charp)
-            free(e_info_charp);
-        if (e_info_charp2)
-            free(e_info_charp2);
+        if (e_info_object)
+            free(e_info_object);
+        if (e_info_schema)
+            free(e_info_schema);
     }

     /* Return the next result, if any, but not if the query failed */
@@ -5037,6 +5068,23 @@ _complete_from_query(const char *simple_query,
 }


+/*
+ * Set up completion_info_object and completion_info_schema
+ * by parsing the given word.  These variables can then be
+ * used in a query passed to _complete_from_query.
+ */
+static void
+setup_completion_info(const char *word)
+{
+    bool        schemaquoted,
+                objectquoted;
+
+    parse_identifier(word,
+                     &completion_info_schema, &completion_info_object,
+                     &schemaquoted, &objectquoted);
+}
+
+
 /*
  * This function returns in order one of a fixed, NULL pointer terminated list
  * of strings (if matching). This can be used if there are only a fixed number
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 176f7e74c2..d13cfc3adf 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4816,123 +4816,125 @@ _complete_from_query(const char *simple_query,
              */
             if (schemaname == NULL || schema_query->namespace == NULL)
             {
-            /* Get unqualified names matching the input-so-far */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer,
-                              "%s, NULL::pg_catalog.text FROM %s",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBufferStr(&query_buffer, " WHERE ");
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
-                              schema_query->result,
-                              object_length, e_objectname);
-            if (schema_query->viscondition)
-            appendPQExpBuffer(&query_buffer, " AND %s",
-                              schema_query->viscondition);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Get unqualified names matching the input-so-far */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer,
+                                  "%s, NULL::pg_catalog.text FROM %s",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
-
-            /*
-             * When fetching relation names, suppress system catalogs unless
-             * the input-so-far begins with "pg_".  This is a compromise
-             * between not offering system catalogs for completion at all, and
-             * having them swamp the result when the input is just "p".
-             */
-            if (strcmp(schema_query->catname,
-                       "pg_catalog.pg_class c") == 0 &&
-                strncmp(objectname, "pg_", 3) != 0)
-            {
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND c.relnamespace <> (SELECT oid FROM"
-                                     " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
-            }
-
-            /*
-             * If the target object type can be schema-qualified, add in
-             * schema names matching the input-so-far.
-             */
-            if (schema_query->namespace)
-            {
-            appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
-                              "SELECT NULL::pg_catalog.text, n.nspname "
-                              "FROM pg_catalog.pg_namespace n "
-                              "WHERE substring(n.nspname,1,%d)='%s'",
-                              object_length, e_objectname);
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBufferStr(&query_buffer, " WHERE ");
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                if (schema_query->viscondition)
+                    appendPQExpBuffer(&query_buffer, " AND %s",
+                                      schema_query->viscondition);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
 
-            /*
-             * Likewise, suppress system schemas unless the input-so-far
-             * begins with "pg_".
-             */
-            if (strncmp(objectname, "pg_", 3) != 0)
-                appendPQExpBufferStr(&query_buffer,
-                                     " AND n.nspname NOT LIKE 'pg\\_%'");
+                /*
+                 * When fetching relation names, suppress system catalogs
+                 * unless the input-so-far begins with "pg_".  This is a
+                 * compromise between not offering system catalogs for
+                 * completion at all, and having them swamp the result when
+                 * the input is just "p".
+                 */
+                if (strcmp(schema_query->catname,
+                           "pg_catalog.pg_class c") == 0 &&
+                    strncmp(objectname, "pg_", 3) != 0)
+                {
+                    appendPQExpBufferStr(&query_buffer,
+                                         " AND c.relnamespace <> (SELECT oid FROM"
+                                         " pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')");
+                }
 
-            /*
-             * Since we're matching these schema names to the object name,
-             * handle their quoting using the object name's quoting state.
-             */
-            schemaquoted = objectquoted;
-            }
+                /*
+                 * If the target object type can be schema-qualified, add in
+                 * schema names matching the input-so-far.
+                 */
+                if (schema_query->namespace)
+                {
+                    appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
+                                      "SELECT NULL::pg_catalog.text, n.nspname "
+                                      "FROM pg_catalog.pg_namespace n "
+                                      "WHERE substring(n.nspname,1,%d)='%s'",
+                                      object_length, e_objectname);
+
+                    /*
+                     * Likewise, suppress system schemas unless the
+                     * input-so-far begins with "pg_".
+                     */
+                    if (strncmp(objectname, "pg_", 3) != 0)
+                        appendPQExpBufferStr(&query_buffer,
+                                             " AND n.nspname NOT LIKE 'pg\\_%'");
+
+                    /*
+                     * Since we're matching these schema names to the object
+                     * name, handle their quoting using the object name's
+                     * quoting state.
+                     */
+                    schemaquoted = objectquoted;
+                }
             }
             else
             {
-            /* Input is qualified, so produce only qualified names */
-            appendPQExpBufferStr(&query_buffer, "SELECT ");
-            if (schema_query->use_distinct)
-                appendPQExpBufferStr(&query_buffer, "DISTINCT ");
-            appendPQExpBuffer(&query_buffer, "%s, n.nspname "
-                              "FROM %s, pg_catalog.pg_namespace n",
-                              schema_query->result,
-                              schema_query->catname);
-            if (schema_query->infonamespace && completion_info_schema)
-                appendPQExpBufferStr(&query_buffer,
-                                     ", pg_catalog.pg_namespace ni");
-            appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
-                              schema_query->namespace);
-            if (schema_query->selcondition)
-                appendPQExpBuffer(&query_buffer, "%s AND ",
-                                  schema_query->selcondition);
-            appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
-                              schema_query->result,
-                              object_length, e_objectname);
-            appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
-                              e_schemaname);
-            if (schema_query->infoname)
-            {
-                Assert(completion_info_object);
-                appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
-                                  schema_query->infoname, e_info_object);
+                /* Input is qualified, so produce only qualified names */
+                appendPQExpBufferStr(&query_buffer, "SELECT ");
+                if (schema_query->use_distinct)
+                    appendPQExpBufferStr(&query_buffer, "DISTINCT ");
+                appendPQExpBuffer(&query_buffer, "%s, n.nspname "
+                                  "FROM %s, pg_catalog.pg_namespace n",
+                                  schema_query->result,
+                                  schema_query->catname);
                 if (schema_query->infonamespace && completion_info_schema)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s = ni.oid AND ni.nspname = '%s'",
-                                      schema_query->infonamespace,
-                                      e_info_schema);
-                else if (schema_query->infoviscondition)
-                    appendPQExpBuffer(&query_buffer,
-                                      " AND %s",
-                                      schema_query->infoviscondition);
-            }
+                    appendPQExpBufferStr(&query_buffer,
+                                         ", pg_catalog.pg_namespace ni");
+                appendPQExpBuffer(&query_buffer, " WHERE %s = n.oid AND ",
+                                  schema_query->namespace);
+                if (schema_query->selcondition)
+                    appendPQExpBuffer(&query_buffer, "%s AND ",
+                                      schema_query->selcondition);
+                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                                  schema_query->result,
+                                  object_length, e_objectname);
+                appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
+                                  e_schemaname);
+                if (schema_query->infoname)
+                {
+                    Assert(completion_info_object);
+                    appendPQExpBuffer(&query_buffer, " AND %s = '%s'",
+                                      schema_query->infoname, e_info_object);
+                    if (schema_query->infonamespace && completion_info_schema)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s = ni.oid AND ni.nspname = '%s'",
+                                          schema_query->infonamespace,
+                                          e_info_schema);
+                    else if (schema_query->infoviscondition)
+                        appendPQExpBuffer(&query_buffer,
+                                          " AND %s",
+                                          schema_query->infoviscondition);
+                }
             }
         }
         else
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index d3d1bd650e..8fda7c7f40 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -40,10 +40,11 @@ $node->start;

 # set up a few database objects
 $node->safe_psql('postgres',
-        "CREATE TABLE tab1 (f1 int, f2 text);\n"
+        "CREATE TABLE tab1 (f1 int primary key, f2 text);\n"
       . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
       . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
-      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz');\n");
+      . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n"
+      . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz', 'BLACK');\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.
@@ -176,6 +177,38 @@ check_completion("2\t", qr/246 /,

 clear_query();

+# check handling of quoted names
+check_completion(
+    "select * from \"my\t",
+    qr/select \* from "my\a?tab/,
+    "complete \"my<tab> to \"mytab when there are multiple choices");
+
+check_completion(
+    "\t\t",
+    qr/"mytab123" +"mytab246"/,
+    "offer multiple quoted table choices");
+
+check_completion("2\t", qr/246" /,
+    "finish completion of one of multiple quoted table choices");
+
+clear_query();
+
+# check handling of mixed-case names
+check_completion(
+    "select * from \"mi\t",
+    qr/"mixedName"/,
+    "complete a mixed-case name");
+
+clear_query();
+
+# check case folding
+check_completion(
+    "select * from TAB\t",
+    qr/tab1 /,
+    "automatically fold case");
+
+clear_query();
+
 # check case-sensitive keyword replacement
 # note: various versions of readline/libedit handle backspacing
 # differently, so just check that the replacement comes out correctly
@@ -183,6 +216,48 @@ check_completion("\\DRD\t", qr/drds /, "complete \\DRD<tab> to \\drds");

 clear_query();

+# check completion of a schema-qualified name
+check_completion(
+    "select * from pub\t",
+    qr/public\./,
+    "complete schema when relevant");
+
+check_completion(
+    "tab\t",
+    qr/tab1 /,
+    "complete schema-qualified name");
+
+clear_query();
+
+check_completion(
+    "select * from PUBLIC.t\t",
+    qr/public\.tab1 /,
+    "automatically fold case in schema-qualified name");
+
+clear_query();
+
+# check interpretation of referenced names
+check_completion(
+    "alter table tab1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table");
+
+clear_query();
+
+check_completion(
+    "alter table TAB1 drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with downcasing");
+
+clear_query();
+
+check_completion(
+    "alter table public.\"tab1\" drop constraint \t",
+    qr/tab1_pkey /,
+    "complete index name for referenced table, with schema and quoting");
+
+clear_query();
+
 # check filename completion
 check_completion(
     "\\lo_import tmp_check/some\t",
@@ -234,6 +309,14 @@ check_completion(

 clear_line();

+# enum labels are case sensitive, so this should complete BLACK immediately
+check_completion(
+    "ALTER TYPE enum1 RENAME VALUE 'B\t",
+    qr|BLACK|,
+    "enum labels are case sensitive");
+
+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 d13cfc3adf..15205a8e08 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -896,10 +896,9 @@ static const SchemaQuery Query_for_trigger_of_table = {
  * name; make a SchemaQuery instead.
  *
  * In these queries, there must be a restriction clause of the form
- *        substring(OUTPUT,1,%d)='%s'
- * where "OUTPUT" is the same string that the query returns.  The %d and %s
- * will be replaced by the string length of the text and the text itself,
- * causing the results to be limited to those matching the already-typed text.
+ *        output LIKE '%s'
+ * where "output" is the same string that the query returns.  The %s
+ * will be replaced by a LIKE pattern to match the already-typed text.
  *
  * There can be a second '%s', which will be replaced by a suitably-escaped
  * version of the string provided in completion_info_object.  If there is a
@@ -912,56 +911,56 @@ static const SchemaQuery Query_for_trigger_of_table = {
 #define Query_for_list_of_template_databases \
 "SELECT d.datname "\
 "  FROM pg_catalog.pg_database d "\
-" WHERE substring(d.datname,1,%d)='%s' "\
+" WHERE d.datname LIKE '%s' "\
 "   AND (d.datistemplate OR pg_catalog.pg_has_role(d.datdba, 'USAGE'))"

 #define Query_for_list_of_databases \
 "SELECT datname FROM pg_catalog.pg_database "\
-" WHERE substring(datname,1,%d)='%s'"
+" WHERE datname LIKE '%s'"

 #define Query_for_list_of_tablespaces \
 "SELECT spcname FROM pg_catalog.pg_tablespace "\
-" WHERE substring(spcname,1,%d)='%s'"
+" WHERE spcname LIKE '%s'"

 #define Query_for_list_of_encodings \
 " SELECT DISTINCT pg_catalog.pg_encoding_to_char(conforencoding) "\
 "   FROM pg_catalog.pg_conversion "\
-"  WHERE substring(pg_catalog.pg_encoding_to_char(conforencoding),1,%d)=UPPER('%s')"
+"  WHERE pg_catalog.pg_encoding_to_char(conforencoding) LIKE UPPER('%s')"

 #define Query_for_list_of_languages \
 "SELECT lanname "\
 "  FROM pg_catalog.pg_language "\
 " WHERE lanname != 'internal' "\
-"   AND substring(lanname,1,%d)='%s'"
+"   AND lanname LIKE '%s'"

 #define Query_for_list_of_schemas \
 "SELECT nspname FROM pg_catalog.pg_namespace "\
-" WHERE substring(nspname,1,%d)='%s'"
+" WHERE nspname LIKE '%s'"

 #define Query_for_list_of_alter_system_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context != 'internal' "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_set_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 "  WHERE context IN ('user', 'superuser') "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_show_vars \
 "SELECT name FROM "\
 " (SELECT pg_catalog.lower(name) AS name FROM pg_catalog.pg_settings "\
 " ) ss "\
-" WHERE substring(name,1,%d)='%s'"
+" WHERE name LIKE '%s'"

 #define Query_for_list_of_roles \
 " SELECT rolname "\
 "   FROM pg_catalog.pg_roles "\
-"  WHERE substring(rolname,1,%d)='%s'"
+"  WHERE rolname LIKE '%s'"

 /* add these to Query_for_list_of_roles in GRANT contexts */
 #define Keywords_for_list_of_grant_roles \
@@ -971,119 +970,119 @@ static const SchemaQuery Query_for_trigger_of_table = {
 "SELECT conname "\
 "  FROM pg_catalog.pg_constraint c "\
 " WHERE c.conrelid <> 0 "\
-"       and substring(conname,1,%d)='%s'"
+"       and conname LIKE '%s'"

 #define Query_for_list_of_ts_configurations \
 "SELECT cfgname FROM pg_catalog.pg_ts_config "\
-" WHERE substring(cfgname,1,%d)='%s'"
+" WHERE cfgname LIKE '%s'"

 #define Query_for_list_of_ts_dictionaries \
 "SELECT dictname FROM pg_catalog.pg_ts_dict "\
-" WHERE substring(dictname,1,%d)='%s'"
+" WHERE dictname LIKE '%s'"

 #define Query_for_list_of_ts_parsers \
 "SELECT prsname FROM pg_catalog.pg_ts_parser "\
-" WHERE substring(prsname,1,%d)='%s'"
+" WHERE prsname LIKE '%s'"

 #define Query_for_list_of_ts_templates \
 "SELECT tmplname FROM pg_catalog.pg_ts_template "\
-" WHERE substring(tmplname,1,%d)='%s'"
+" WHERE tmplname LIKE '%s'"

 #define Query_for_list_of_fdws \
 " SELECT fdwname "\
 "   FROM pg_catalog.pg_foreign_data_wrapper "\
-"  WHERE substring(fdwname,1,%d)='%s'"
+"  WHERE fdwname LIKE '%s'"

 #define Query_for_list_of_servers \
 " SELECT srvname "\
 "   FROM pg_catalog.pg_foreign_server "\
-"  WHERE substring(srvname,1,%d)='%s'"
+"  WHERE srvname LIKE '%s'"

 #define Query_for_list_of_user_mappings \
 " SELECT usename "\
 "   FROM pg_catalog.pg_user_mappings "\
-"  WHERE substring(usename,1,%d)='%s'"
+"  WHERE usename LIKE '%s'"

 #define Query_for_list_of_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s'"
+"  WHERE amname LIKE '%s'"

 #define Query_for_list_of_index_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s' AND "\
+"  WHERE amname LIKE '%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_INDEX)

 #define Query_for_list_of_table_access_methods \
 " SELECT amname "\
 "   FROM pg_catalog.pg_am "\
-"  WHERE substring(amname,1,%d)='%s' AND "\
+"  WHERE amname LIKE '%s' AND "\
 "   amtype=" CppAsString2(AMTYPE_TABLE)

 #define Query_for_list_of_extensions \
 " SELECT extname "\
 "   FROM pg_catalog.pg_extension "\
-"  WHERE substring(extname,1,%d)='%s'"
+"  WHERE extname LIKE '%s'"

 #define Query_for_list_of_available_extensions \
 " SELECT name "\
 "   FROM pg_catalog.pg_available_extensions "\
-"  WHERE substring(name,1,%d)='%s' AND installed_version IS NULL"
+"  WHERE name LIKE '%s' AND installed_version IS NULL"

 /* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions \
 " SELECT version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE substring(version,1,%d)='%s'"\
+"  WHERE version LIKE '%s'"\
 "    AND name='%s'"

 /* the result of this query is not an identifier, so use VERBATIM */
 #define Query_for_list_of_available_extension_versions_with_TO \
 " SELECT 'TO ' || version "\
 "   FROM pg_catalog.pg_available_extension_versions "\
-"  WHERE substring('TO ' || version,1,%d)='%s'"\
+"  WHERE ('TO ' || version) LIKE '%s'"\
 "    AND name='%s'"

 #define Query_for_list_of_prepared_statements \
 " SELECT name "\
 "   FROM pg_catalog.pg_prepared_statements "\
-"  WHERE substring(name,1,%d)='%s'"
+"  WHERE name LIKE '%s'"

 #define Query_for_list_of_event_triggers \
 " SELECT evtname "\
 "   FROM pg_catalog.pg_event_trigger "\
-"  WHERE substring(evtname,1,%d)='%s'"
+"  WHERE evtname LIKE '%s'"

 #define Query_for_list_of_tablesample_methods \
 " SELECT proname "\
 "   FROM pg_catalog.pg_proc "\
 "  WHERE prorettype = 'pg_catalog.tsm_handler'::pg_catalog.regtype AND "\
 "        proargtypes[0] = 'pg_catalog.internal'::pg_catalog.regtype AND "\
-"        substring(proname,1,%d)='%s'"
+"        proname LIKE '%s'"

 #define Query_for_list_of_policies \
 " SELECT polname "\
 "   FROM pg_catalog.pg_policy "\
-"  WHERE substring(polname,1,%d)='%s'"
+"  WHERE polname LIKE '%s'"

 #define Query_for_values_of_enum_GUC \
 " SELECT val FROM ( "\
 "   SELECT name, pg_catalog.unnest(enumvals) AS val "\
 "     FROM pg_catalog.pg_settings "\
 "    ) ss "\
-"  WHERE substring(val,1,%d)='%s'"\
+"  WHERE val LIKE '%s'"\
 "        and pg_catalog.lower(name)=pg_catalog.lower('%s')"

 #define Query_for_list_of_channels \
 " SELECT channel "\
 "   FROM pg_catalog.pg_listening_channels() AS channel "\
-"  WHERE substring(channel,1,%d)='%s'"
+"  WHERE channel LIKE '%s'"

 #define Query_for_list_of_cursors \
 " SELECT name "\
 "   FROM pg_catalog.pg_cursors "\
-"  WHERE substring(name,1,%d)='%s'"
+"  WHERE name LIKE '%s'"

 /*
  * These object types were introduced later than our support cutoff of
@@ -1095,7 +1094,7 @@ static const VersionedQuery Query_for_list_of_publications[] = {
     {100000,
         " SELECT pubname "
         "   FROM pg_catalog.pg_publication "
-        "  WHERE substring(pubname,1,%d)='%s'"
+        "  WHERE pubname LIKE '%s'"
     },
     {0, NULL}
 };
@@ -1104,7 +1103,7 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
     {100000,
         " SELECT s.subname "
         "   FROM pg_catalog.pg_subscription s, pg_catalog.pg_database d "
-        "  WHERE substring(s.subname,1,%d)='%s' "
+        "  WHERE s.subname LIKE '%s' "
         "    AND d.datname = pg_catalog.current_database() "
         "    AND s.subdbid = d.oid"
     },
@@ -1150,7 +1149,7 @@ static const pgsql_thing_t words_after_create[] = {
      * to be used only by pg_dump.
      */
     {"CONFIGURATION", Query_for_list_of_ts_configurations, NULL, NULL, NULL, THING_NO_SHOW},
-    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE substring(conname,1,%d)='%s'"},
+    {"CONVERSION", "SELECT conname FROM pg_catalog.pg_conversion WHERE conname LIKE '%s'"},
     {"DATABASE", Query_for_list_of_databases},
     {"DEFAULT PRIVILEGES", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
     {"DICTIONARY", Query_for_list_of_ts_dictionaries, NULL, NULL, NULL, THING_NO_SHOW},
@@ -1175,7 +1174,7 @@ static const pgsql_thing_t words_after_create[] = {
     {"PUBLICATION", NULL, Query_for_list_of_publications},
     {"ROLE", Query_for_list_of_roles},
     {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
-    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE substring(rulename,1,%d)='%s'"},
+    {"RULE", "SELECT rulename FROM pg_catalog.pg_rules WHERE rulename LIKE '%s'"},
     {"SCHEMA", Query_for_list_of_schemas},
     {"SEQUENCE", NULL, NULL, &Query_for_list_of_sequences},
     {"SERVER", Query_for_list_of_servers},
@@ -1191,7 +1190,7 @@ static const pgsql_thing_t words_after_create[] = {
                                                                              * TABLE ... */
     {"TEXT SEARCH", NULL, NULL, NULL},
     {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER},
-    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE substring(tgname,1,%d)='%s' AND NOT tgisinternal"},
+    {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"},
     {"TYPE", NULL, NULL, &Query_for_list_of_datatypes},
     {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE
                                                                          * INDEX ... */
@@ -1273,6 +1272,7 @@ static char *complete_from_files(const char *text, int state);

 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
+static char *make_like_pattern(const char *word);
 static void parse_identifier(const char *ident,
                              char **schemaname, char **objectname,
                              bool *schemaquoted, bool *objectquoted);
@@ -1775,9 +1775,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("(", "ALL TABLES IN SCHEMA", "TABLE");
     else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "ALL", "TABLES", "IN", "SCHEMA"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                 " AND nspname != 'pg_catalog' "
-                                 " AND nspname not like 'pg\\_toast%%' "
-                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 " AND nspname NOT LIKE E'pg\\\\_%'",
                                  "CURRENT_SCHEMA");
     /* ALTER PUBLICATION <name> SET ( */
     else if (HeadMatches("ALTER", "PUBLICATION", MatchAny) && TailMatches("SET", "("))
@@ -2911,9 +2909,7 @@ psql_completion(const char *text, int start, int end)
      */
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                 " AND nspname != 'pg_catalog' "
-                                 " AND nspname not like 'pg\\_toast%%' "
-                                 " AND nspname not like 'pg\\_temp%%' ",
+                                 " AND nspname NOT LIKE E'pg\\\\_%'",
                                  "CURRENT_SCHEMA");
     else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "IN", "SCHEMA", MatchAny) &&
(!ends_with(prev_wd,','))) 
         COMPLETE_WITH("WITH (");
@@ -4119,10 +4115,13 @@ psql_completion(const char *text, int start, int end)
                           "US", "European", "NonEuropean",
                           "DEFAULT");
         else if (TailMatches("search_path", "TO|="))
+        {
+            /* Here, we want to allow pg_catalog, so use narrower exclusion */
             COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas
-                                     " AND nspname not like 'pg\\_toast%%' "
-                                     " AND nspname not like 'pg\\_temp%%' ",
+                                     " AND nspname NOT LIKE E'pg\\\\_toast%%'"
+                                     " AND nspname NOT LIKE E'pg\\\\_temp%%'",
                                      "DEFAULT");
+        }
         else
         {
             /* generic, type based, GUC support */
@@ -4689,10 +4688,9 @@ complete_from_versioned_schema_query(const char *text, int state)
  * The query can be one of two kinds:
  *
  * 1. A simple query, which must contain a restriction clause of the form
- *        substring(OUTPUT,1,%d)='%s'
- * where "OUTPUT" is the same string that the query returns.  The %d and %s
- * will be replaced by the string length of the text and the text itself,
- * causing the results to be limited to those matching the already-typed text.
+ *        output LIKE '%s'
+ * where "output" is the same string that the query returns.  The %s
+ * will be replaced by a LIKE pattern to match the already-typed text.
  * There can be a second '%s', which will be replaced by a suitably-escaped
  * version of the string provided in completion_info_object.  If there is a
  * third '%s', it will be replaced by a suitably-escaped version of the string
@@ -4747,9 +4745,8 @@ _complete_from_query(const char *simple_query,
         PQExpBufferData query_buffer;
         char       *schemaname;
         char       *objectname;
-        int            object_length = 0;
+        char       *e_object_like;
         char       *e_schemaname;
-        char       *e_objectname;
         char       *e_info_object;
         char       *e_info_schema;

@@ -4777,22 +4774,16 @@ _complete_from_query(const char *simple_query,
         non_empty_object = (*objectname != '\0');

         /*
-         * Count length as number of characters (not bytes), for passing to
-         * substring
+         * Convert objectname to a LIKE prefix pattern (e.g. 'foo%'), and set
+         * up suitably-escaped copies of all the strings we need.
          */
-        for (const char *p = objectname;
-             *p;
-             p += PQmblenBounded(p, pset.encoding))
-            object_length++;
+        e_object_like = make_like_pattern(objectname);

-        /* Set up suitably-escaped copies of textual inputs */
         if (schemaname)
             e_schemaname = escape_string(schemaname);
         else
             e_schemaname = NULL;

-        e_objectname = escape_string(objectname);
-
         if (completion_info_object)
             e_info_object = escape_string(completion_info_object);
         else
@@ -4831,9 +4822,9 @@ _complete_from_query(const char *simple_query,
                 if (schema_query->selcondition)
                     appendPQExpBuffer(&query_buffer, "%s AND ",
                                       schema_query->selcondition);
-                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s'",
+                appendPQExpBuffer(&query_buffer, "(%s) LIKE '%s'",
                                   schema_query->result,
-                                  object_length, e_objectname);
+                                  e_object_like);
                 if (schema_query->viscondition)
                     appendPQExpBuffer(&query_buffer, " AND %s",
                                       schema_query->viscondition);
@@ -4878,8 +4869,8 @@ _complete_from_query(const char *simple_query,
                     appendPQExpBuffer(&query_buffer, "\nUNION ALL\n"
                                       "SELECT NULL::pg_catalog.text, n.nspname "
                                       "FROM pg_catalog.pg_namespace n "
-                                      "WHERE substring(n.nspname,1,%d)='%s'",
-                                      object_length, e_objectname);
+                                      "WHERE n.nspname LIKE '%s'",
+                                      e_object_like);

                     /*
                      * Likewise, suppress system schemas unless the
@@ -4887,7 +4878,7 @@ _complete_from_query(const char *simple_query,
                      */
                     if (strncmp(objectname, "pg_", 3) != 0)
                         appendPQExpBufferStr(&query_buffer,
-                                             " AND n.nspname NOT LIKE 'pg\\_%'");
+                                             " AND n.nspname NOT LIKE E'pg\\\\_%'");

                     /*
                      * Since we're matching these schema names to the object
@@ -4915,9 +4906,9 @@ _complete_from_query(const char *simple_query,
                 if (schema_query->selcondition)
                     appendPQExpBuffer(&query_buffer, "%s AND ",
                                       schema_query->selcondition);
-                appendPQExpBuffer(&query_buffer, "substring(%s,1,%d)='%s' AND ",
+                appendPQExpBuffer(&query_buffer, "(%s) LIKE '%s' AND ",
                                   schema_query->result,
-                                  object_length, e_objectname);
+                                  e_object_like);
                 appendPQExpBuffer(&query_buffer, "n.nspname = '%s'",
                                   e_schemaname);
                 if (schema_query->infoname)
@@ -4942,7 +4933,7 @@ _complete_from_query(const char *simple_query,
             Assert(simple_query);
             /* simple_query is an sprintf-style format string */
             appendPQExpBuffer(&query_buffer, simple_query,
-                              object_length, e_objectname,
+                              e_object_like,
                               e_info_object, e_info_schema);
         }

@@ -4955,9 +4946,9 @@ _complete_from_query(const char *simple_query,

         /* Clean up */
         termPQExpBuffer(&query_buffer);
+        free(e_object_like);
         if (e_schemaname)
             free(e_schemaname);
-        free(e_objectname);
         if (e_info_object)
             free(e_info_object);
         if (e_info_schema)
@@ -5414,6 +5405,48 @@ escape_string(const char *text)
 }


+/*
+ * make_like_pattern - Convert argument to a LIKE prefix pattern.
+ *
+ * We escape _ and % in the given text by backslashing, append a % to
+ * represent "any subsequent characters", and then pass the string through
+ * escape_string() so it's ready to insert in a query.  The result needs
+ * to be freed.
+ */
+static char *
+make_like_pattern(const char *word)
+{
+    char       *result;
+    char       *buffer = pg_malloc(strlen(word) * 2 + 2);
+    char       *bptr = buffer;
+
+    while (*word)
+    {
+        if (*word == '_' || *word == '%')
+            *bptr++ = '\\';
+        if (IS_HIGHBIT_SET(*word))
+        {
+            /*
+             * Transfer multibyte characters without further processing, to
+             * avoid getting confused in unsafe client encodings.
+             */
+            int            chlen = PQmblenBounded(word, pset.encoding);
+
+            while (chlen-- > 0)
+                *bptr++ = *word++;
+        }
+        else
+            *bptr++ = *word++;
+    }
+    *bptr++ = '%';
+    *bptr = '\0';
+
+    result = escape_string(buffer);
+    free(buffer);
+    return result;
+}
+
+
 /*
  * parse_identifier - Parse a possibly-schema-qualified SQL identifier.
  *

RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Saturday, January 29, 2022 1:03 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> "tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
> > I did some tests on it and here are something cases I feel we need to confirm
> > whether they are suitable.
>
> > 1) postgres=# create table atest(id int, "iD" int, "ID" int);
> > 2) CREATE TABLE
> > 3) postgres=# alter table atest rename i[TAB]
> > 4) id    "iD"
> > 5) postgres=# alter table atest rename I[TAB]
> > 6) id    "iD"
>
> > The tab completion for 5) ignored "ID", is that correct?
>
> Perhaps I misunderstood your original complaint, but what I thought
> you were unhappy about was that unquoted ID is a legal spelling of
> "id" and so I<TAB> ought to be willing to complete that.  These
> examples with case variants of the same word are of some interest,
> but people aren't really going to create tables with these sorts of
> names, so we shouldn't let them drive the design IMO.
>
> Anyway, the existing behavior for these examples is
>
> alter table atest rename i<TAB> --- completes immediately to id
> alter table atest rename I<TAB> --- offers nothing
>
> It's certainly arguable that the first case is right as-is and we
> shouldn't change it.  I think that could be handled by tweaking my
> patch so that it wouldn't offer completions that start with a quote
> unless the input word does.  That would also cause I<TAB> to complete
> immediately to id, which is arguably fine.
>
> > I think what we are trying to do is to ease the burden of typing double quote
> for user.
>
> I'm not thinking about it that way at all.  To me, the goal is to make
> tab completion do something sensible when presented with legal variant
> spellings of a word.  The two cases where it currently fails to do
> that are (1) unquoted input that needs to be downcased, and (2) input
> that is quoted when it doesn't strictly need to be.
>
> To the extent that we can supply a required quote that the user
> failed to type, that's fine, but it's not a primary goal of the patch.
> Examples like these make me question whether it's even something we
> want; it's resulting in extraneous matches that people might find more
> annoying than helpful.  Now I *think* that these aren't realistic
> cases and that in real cases adding quotes will be helpful more often
> than not, but it's debatable.
>
> > One the other hand, I'm not so comfortable with the output of "iD" in line
> 13.
> > If user doesn't type double quote, why we add double quote to the output?
>
> That's certainly a valid argument.
>
> > Could we make the output of 13) like below?
> > 12) postgres=# alter table atest rename i[TAB]
> > ??) id  iD
>
> That doesn't seem sensible at all.

Thanks for your kindly explanation.
I'm fine with the current tap completion style with your V16 patch.

Regards,
Tang



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Saturday, January 29, 2022 7:17 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Sigh ... per the cfbot, this was already blindsided by 95787e849.
> As I said, I don't want to sit on this for very long.

Thanks for your V16 patch, I tested it.
The results LGTM.

Regards,
Tang



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
"tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
> Thanks for your V16 patch, I tested it. 
> The results LGTM.

Pushed, thanks for looking.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Dagfinn Ilmari Mannsåker
Дата:
Tom Lane <tgl@sss.pgh.pa.us> writes:

> "tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
>> Thanks for your V16 patch, I tested it. 
>> The results LGTM.
>
> Pushed, thanks for looking.

I wasn't following this thread, but I noticed a few small potential
improvements when I saw the commit.

First, as noted in the test, it doesn't preserve the case of the input
for keywords appended to the query result.  This is easily fixed by
using `pg_strdup_keyword_case()`, per the first attached patch.

The second might be more of a matter of style or opinion, but I noticed
a bunch of `if (foo) free(foo);`, which is redundant given that
`free(NULL)` is a no-op.  To simplify the code further, I also made
`escape_string(NULL)` be a no-op, returning `NULL`.

- ilmari


Вложения

Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
=?utf-8?Q?Dagfinn_Ilmari_Manns=C3=A5ker?= <ilmari@ilmari.org> writes:
> First, as noted in the test, it doesn't preserve the case of the input
> for keywords appended to the query result.  This is easily fixed by
> using `pg_strdup_keyword_case()`, per the first attached patch.

I thought about that, and intentionally didn't do it, because it
would also affect the menus produced by tab completion.  Currently,
keywords are (usually) visually distinct from non-keywords in those
menus, thanks to being upper-case where the object names usually
aren't:

regression=# create table foo (c1 int, c2 int);            
CREATE TABLE
regression=# alter table foo rename c<TAB>
c1          c2          COLUMN      CONSTRAINT  

With this change, the keywords would be visually indistinguishable
from the object names, which I felt wouldn't be a net improvement.

We could do something hacky like matching case only when there's
no longer any matching object names, but that might be too magic.

> The second might be more of a matter of style or opinion, but I noticed
> a bunch of `if (foo) free(foo);`, which is redundant given that
> `free(NULL)` is a no-op.  To simplify the code further, I also made
> `escape_string(NULL)` be a no-op, returning `NULL`.

Yeah.  Our fairly longstanding convention is to avoid doing
free(NULL), dating back to when some platforms would crash on it.
I realize that's archaic now, but I'm not inclined to change
it in just some places.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
I wrote:
> =?utf-8?Q?Dagfinn_Ilmari_Manns=C3=A5ker?= <ilmari@ilmari.org> writes:
>> First, as noted in the test, it doesn't preserve the case of the input
>> for keywords appended to the query result.  This is easily fixed by
>> using `pg_strdup_keyword_case()`, per the first attached patch.

> I thought about that, and intentionally didn't do it, because it
> would also affect the menus produced by tab completion.
> ...
> We could do something hacky like matching case only when there's
> no longer any matching object names, but that might be too magic.

I experimented with that, and it actually doesn't seem as weird
as I feared.  See if you like this ...

            regards, tom lane

diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index c4f6552ac9..7a265e0676 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -40,7 +40,7 @@ $node->start;

 # set up a few database objects
 $node->safe_psql('postgres',
-        "CREATE TABLE tab1 (f1 int primary key, f2 text);\n"
+        "CREATE TABLE tab1 (c1 int primary key, c2 text);\n"
       . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
       . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
       . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n"
@@ -317,14 +317,30 @@ check_completion(

 clear_line();

-# check completion of a keyword offered in addition to object names
-# (that code path currently doesn't preserve case of what's typed)
-check_completion(
-    "comment on constraint foo on dom\t",
-    qr|DOMAIN|,
-    "offer keyword in addition to query result");
-
-clear_query();
+# check completion of a keyword offered in addition to object names;
+# such a keyword should obey COMP_KEYWORD_CASE once only keyword
+# completions are possible
+foreach (
+    [ 'lower',          'CO', 'column' ],
+    [ 'upper',          'co', 'COLUMN' ],
+    [ 'preserve-lower', 'co', 'column' ],
+    [ 'preserve-upper', 'CO', 'COLUMN' ],)
+{
+    my ($case, $in, $out) = @$_;
+
+    check_completion(
+        "\\set COMP_KEYWORD_CASE $case\n",
+        qr/postgres=#/,
+        "set completion case to '$case'");
+    check_completion("alter table tab1 rename c\t\t",
+        qr|COLUMN|,
+        "offer keyword COLUMN for input c<TAB>, COMP_KEYWORD_CASE = $case");
+    clear_query();
+    check_completion("alter table tab1 rename $in\t\t\t",
+        qr|$out|,
+        "offer keyword $out for input $in<TAB>, COMP_KEYWORD_CASE = $case");
+    clear_query();
+}

 # send psql an explicit \q to shut it down, else pty won't close properly
 $timer->start(5);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b2ec50b4f2..bdc9760fba 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4742,7 +4742,8 @@ _complete_from_query(const char *simple_query,
 {
     static int    list_index,
                 num_schema_only,
-                num_other;
+                num_other,
+                num_keywords;
     static PGresult *result = NULL;
     static bool non_empty_object;
     static bool schemaquoted;
@@ -4766,6 +4767,7 @@ _complete_from_query(const char *simple_query,
         list_index = 0;
         num_schema_only = 0;
         num_other = 0;
+        num_keywords = 0;
         PQclear(result);
         result = NULL;

@@ -4986,7 +4988,10 @@ _complete_from_query(const char *simple_query,

             /* In verbatim mode, we return all the items as-is */
             if (verbatim)
+            {
+                num_other++;
                 return pg_strdup(item);
+            }

             /*
              * In normal mode, a name requiring quoting will be returned only
@@ -5031,8 +5036,12 @@ _complete_from_query(const char *simple_query,
                 list_index++;
                 if (pg_strncasecmp(text, item, strlen(text)) == 0)
                 {
-                    num_other++;
-                    return pg_strdup(item);
+                    num_keywords++;
+                    /* Match keyword case if we are returning only keywords */
+                    if (num_schema_only == 0 && num_other == 0)
+                        return pg_strdup_keyword_case(item, text);
+                    else
+                        return pg_strdup(item);
                 }
             }
         }
@@ -5049,8 +5058,12 @@ _complete_from_query(const char *simple_query,
                 list_index++;
                 if (pg_strncasecmp(text, item, strlen(text)) == 0)
                 {
-                    num_other++;
-                    return pg_strdup(item);
+                    num_keywords++;
+                    /* Match keyword case if we are returning only keywords */
+                    if (num_schema_only == 0 && num_other == 0)
+                        return pg_strdup_keyword_case(item, text);
+                    else
+                        return pg_strdup(item);
                 }
             }
         }
@@ -5062,7 +5075,7 @@ _complete_from_query(const char *simple_query,
      * completion subject text, which is not what we want.
      */
 #ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
-    if (num_schema_only > 0 && num_other == 0)
+    if (num_schema_only > 0 && num_other == 0 && num_keywords == 0)
         rl_completion_append_character = '\0';
 #endif


Re: Support tab completion for upper character inputs in psql

От
Dagfinn Ilmari Mannsåker
Дата:
Tom Lane <tgl@sss.pgh.pa.us> writes:

> I wrote:
>> =?utf-8?Q?Dagfinn_Ilmari_Manns=C3=A5ker?= <ilmari@ilmari.org> writes:
>>> First, as noted in the test, it doesn't preserve the case of the input
>>> for keywords appended to the query result.  This is easily fixed by
>>> using `pg_strdup_keyword_case()`, per the first attached patch.
>
>> I thought about that, and intentionally didn't do it, because it
>> would also affect the menus produced by tab completion.
>> ...
>> We could do something hacky like matching case only when there's
>> no longer any matching object names, but that might be too magic.
>
> I experimented with that, and it actually doesn't seem as weird
> as I feared.  See if you like this ...

That's a reasonable compromise, and the implementation is indeed less
hacky than one might have feared.  Although I think putting the
`num_keywords` variable before `num_other` would read better.

Going through the uses of COMPLETE_WITH(_SCHEMA)_QUERY_PLUS, I noticed a
few that had the keywords in lower case, which is fixed in the attached
patch (except the hardcoded data types, which aren't really keywords).
While I was there, I also added completion of "AUTHORIZATION" after
"SHOW SESSSION", which is necessary since there are variables starting
with "session_".

>             regards, tom lane

Cheers,
- ilmari

From 9cc255e0cdddeda3d655c1265d698b1f6027aec6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dagfinn=20Ilmari=20Manns=C3=A5ker?= <ilmari@ilmari.org>
Date: Tue, 1 Feb 2022 13:13:07 +0000
Subject: [PATCH] Make all tab completion keywords upper case.

So they stand out from the object names in the same tab completion
menu.

Also fix tab completion of SHOW SESSION AUTHORIZATION in the face of
config variables starting with session_.
---
 src/bin/psql/tab-complete.c | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b2ec50b4f2..d2744cdb6f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2059,7 +2059,7 @@ psql_completion(const char *text, int start, int end)
         COMPLETE_WITH("SET", "RESET");
     else if (Matches("ALTER", "SYSTEM", "SET|RESET"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_alter_system_set_vars,
-                                 "all");
+                                 "ALL");
     else if (Matches("ALTER", "SYSTEM", "SET", MatchAny))
         COMPLETE_WITH("TO");
     /* ALTER VIEW <name> */
@@ -4039,16 +4039,18 @@ psql_completion(const char *text, int start, int end)
     /* Complete with a variable name */
     else if (TailMatches("SET|RESET") && !TailMatches("UPDATE", MatchAny, "SET"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_set_vars,
-                                 "constraints",
-                                 "transaction",
-                                 "session",
-                                 "role",
-                                 "tablespace",
-                                 "all");
+                                 "CONSTRAINTS",
+                                 "TRANSACTION",
+                                 "SESSION",
+                                 "ROLE",
+                                 "TABLESPACE",
+                                 "ALL");
     else if (Matches("SHOW"))
         COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_show_vars,
-                                 "session authorization",
-                                 "all");
+                                 "SESSION AUTHORIZATION",
+                                 "ALL");
+    else if (Matches("SHOW", "SESSION"))
+        COMPLETE_WITH("AUTHORIZATION");
     /* Complete "SET TRANSACTION" */
     else if (Matches("SET", "TRANSACTION"))
         COMPLETE_WITH("SNAPSHOT", "ISOLATION LEVEL", "READ", "DEFERRABLE", "NOT DEFERRABLE");
-- 
2.30.2


Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
=?utf-8?Q?Dagfinn_Ilmari_Manns=C3=A5ker?= <ilmari@ilmari.org> writes:
> Tom Lane <tgl@sss.pgh.pa.us> writes:
>>> We could do something hacky like matching case only when there's
>>> no longer any matching object names, but that might be too magic.
>> I experimented with that, and it actually doesn't seem as weird
>> as I feared.  See if you like this ...

> That's a reasonable compromise, and the implementation is indeed less
> hacky than one might have feared.  Although I think putting the
> `num_keywords` variable before `num_other` would read better.

Hm... I renamed "num_other" to "num_query_other" instead.

> Going through the uses of COMPLETE_WITH(_SCHEMA)_QUERY_PLUS, I noticed a
> few that had the keywords in lower case, which is fixed in the attached
> patch (except the hardcoded data types, which aren't really keywords).

Yeah, my oversight.  Pushed.

            regards, tom lane



Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
=?utf-8?Q?Dagfinn_Ilmari_Manns=C3=A5ker?= <ilmari@ilmari.org> writes:
> Tom Lane <tgl@sss.pgh.pa.us> writes:
>>> We could do something hacky like matching case only when there's
>>> no longer any matching object names, but that might be too magic.

>> I experimented with that, and it actually doesn't seem as weird
>> as I feared.  See if you like this ...

> That's a reasonable compromise, and the implementation is indeed less
> hacky than one might have feared.  Although I think putting the
> `num_keywords` variable before `num_other` would read better.

After a few days of using that, I'm having second thoughts about it,
because it turns out to impede completion in common cases.  For
example,

regression=# set transa<TAB><TAB>
TRANSACTION             transaction_isolation   
transaction_deferrable  transaction_read_only   

It won't fill in "ction" because of the case discrepancy between the
offered alternatives.  Maybe this trumps the question of whether you
should be able to distinguish keywords from non-keywords in the menus.
If we case-folded the keywords as per your original proposal, it'd do
what I expect it to.

In previous releases, this worked as expected: "set transa<TAB>"
immediately completes "ction", and then tabbing produces this
menu:

transaction             transaction_isolation   
transaction_deferrable  transaction_read_only   

That probably explains why these keywords were lower-cased in
the previous code.  However, I don't think we should blame
your suggestion to upcase them, because the same problem arises
in other completion contexts where we offer keywords.  We should
solve it across-the-board not just for these specific queries.

            regards, tom lane



RE: Support tab completion for upper character inputs in psql

От
"tanghy.fnst@fujitsu.com"
Дата:
On Monday, January 31, 2022 3:35 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> "tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
> > Thanks for your V16 patch, I tested it.
> > The results LGTM.
>
> Pushed, thanks for looking.

I think 02b8048 forgot to free some used memory.
Attached a tiny patch to fix it. Please have a check.

Regards,
Tang

Вложения

Re: Support tab completion for upper character inputs in psql

От
Tom Lane
Дата:
"tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:
> I think 02b8048 forgot to free some used memory. 
> Attached a tiny patch to fix it. Please have a check.

Right you are.  Inspired by that, I tried running some tab-completion
operations under valgrind, and found another nearby leak in
patternToSQLRegex.  Fixes pushed.

            regards, tom lane