[PATCH] support tab-completion for single quote input with equal sign

Started by tanghy.fnst@fujitsu.comover 4 years ago19 messages
#1tanghy.fnst@fujitsu.com
tanghy.fnst@fujitsu.com
1 attachment(s)

Hi

I found a problem when using tab-completion as follows:

CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=localhost port=5432 dbname=postgres' [TAB]

The word 'PUBLICATION' couldn't be auto completed as expected.

The reason is that the equal sign in a single quote is taken as WORD_BREAKS.
Which causes words count is not correct for a input command string.

Fix the problem in the attached patch. By fix this, "\t\n@$><=;|&{() " will not be taken as WORD_BREAKS in single quoted input string.
Please be kindly to take a look at the fix and kindly to tell me if you find any scenario affected by the fix.

Regards,
Tang

Attachments:

0001-support-tab-completion-for-single-quote-input-with-e.patchapplication/octet-stream; name=0001-support-tab-completion-for-single-quote-input-with-e.patchDownload
From 70e542a4db3493fc519293caeaa401843694d201 Mon Sep 17 00:00:00 2001
From: tanghy <tanghy.fnst@fujitsu.com>
Date: Wed, 21 Jul 2021 21:46:49 +0900
Subject: [PATCH] support tab-completion for single quote input with equal sign


diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d6bf725971..3338e4eb04 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4984,6 +4984,7 @@ get_previous_words(int point, char **buffer, int *nwords)
 		int			start,
 					end;
 		bool		inquotes = false;
+		bool		in_single_quote = false;
 		int			parentheses = 0;
 
 		/* now find the first non-space which then constitutes the end */
@@ -5012,6 +5013,8 @@ get_previous_words(int point, char **buffer, int *nwords)
 				inquotes = !inquotes;
 			if (!inquotes)
 			{
+				if (buf[start] == '\'')
+					in_single_quote = !in_single_quote;
 				if (buf[start] == ')')
 					parentheses++;
 				else if (buf[start] == '(')
@@ -5020,6 +5023,7 @@ get_previous_words(int point, char **buffer, int *nwords)
 						break;
 				}
 				else if (parentheses == 0 &&
+						 !in_single_quote &&
 						 strchr(WORD_BREAKS, buf[start - 1]))
 					break;
 			}
-- 
2.31.1.windows.1

#2tanghy.fnst@fujitsu.com
tanghy.fnst@fujitsu.com
In reply to: tanghy.fnst@fujitsu.com (#1)
RE: [PATCH] support tab-completion for single quote input with equal sign

On Thursday, July 22, 2021 1:05 PM, tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com> wrote

I found a problem when using tab-completion as follows:

CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=localhost port=5432 dbname=postgres' [TAB]

The word 'PUBLICATION' couldn't be auto completed as expected.

Added above patch in commit fest as follows:

https://commitfest.postgresql.org/34/3267/

Regards,
Tang

#3Jacob Champion
pchampion@vmware.com
In reply to: tanghy.fnst@fujitsu.com (#2)
Re: [PATCH] support tab-completion for single quote input with equal sign

On Fri, 2021-07-23 at 05:34 +0000, tanghy.fnst@fujitsu.com wrote:

On Thursday, July 22, 2021 1:05 PM, tanghy.fnst@fujitsu.com <tanghy.fnst@fujitsu.com> wrote

I found a problem when using tab-completion as follows:

CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=localhost port=5432 dbname=postgres' [TAB]

The word 'PUBLICATION' couldn't be auto completed as expected.

Hello,

I applied your patch against HEAD (and did a clean build for good
measure) but couldn't get the tab-completion you described -- on my
machine, `PUBLICATION` still fails to complete. Tab completion is
working in general, for example with the `SUBSCRIPTION` and
`CONNECTION` keywords.

Is there additional setup that I need to do?

--Jacob

#4tanghy.fnst@fujitsu.com
tanghy.fnst@fujitsu.com
In reply to: Jacob Champion (#3)
1 attachment(s)
RE: [PATCH] support tab-completion for single quote input with equal sign

On Friday, September 3, 2021 2:14 AM, Jacob Champion <pchampion@vmware.com> wrote

I applied your patch against HEAD (and did a clean build for good
measure) but couldn't get the tab-completion you described -- on my
machine, `PUBLICATION` still fails to complete. Tab completion is
working in general, for example with the `SUBSCRIPTION` and
`CONNECTION` keywords.

Is there additional setup that I need to do?

Thanks for your check.

I applied the 0001-patch to the HEAD(c95ede41) by now and it worked as I expected.
Did you leave a space between "dbname=postgres'" and "[TAB]"?

In 0002-patch I added a tap test for the scenario of single quoted input with equal sign.
The test result is 'All tests successful. ' on my machine.
You can run the tap tests as follows:
1. Apply the attached patch
2. build the src with option '--enable-tap-tests' (./configure --enable-tap-tests)
3. Move to src subdirectory 'src/bin/psql'
4. Run 'make check'

I'd appreciate it if you can share your test results with me.

Regards,
Tang

Attachments:

v2-0001-support-tab-completion-for-single-quote-input-wit.patchapplication/octet-stream; name=v2-0001-support-tab-completion-for-single-quote-input-wit.patchDownload
From 78636e41582eae4f46880d4a0469ed3832163ac5 Mon Sep 17 00:00:00 2001
From: tanghy <tanghy.fnst@fujitsu.com>
Date: Fri, 3 Sep 2021 13:17:46 +0900
Subject: [PATCH v2] support tab-completion for single quote input with equal
 sign

---
 src/bin/psql/t/010_tab_completion.pl | 8 ++++++++
 src/bin/psql/tab-complete.c          | 4 ++++
 2 files changed, 12 insertions(+)

diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 8695d22545..cfebb654d8 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -194,6 +194,14 @@ check_completion(
 
 clear_query();
 
+# check tab-completion after single quoted text input with equal sign.
+check_completion(
+	"CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \t",
+	qr|PUBLICATION|,
+	"tab-completion after single quoted text input with equal sign");
+
+clear_query();
+
 # COPY requires quoting
 # note: broken versions of libedit want to backslash the closing quote;
 # not much we can do about that
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 75b867685a..b9373e4712 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -5044,6 +5044,7 @@ get_previous_words(int point, char **buffer, int *nwords)
 		int			start,
 					end;
 		bool		inquotes = false;
+		bool		in_single_quote = false;
 		int			parentheses = 0;
 
 		/* now find the first non-space which then constitutes the end */
@@ -5072,6 +5073,8 @@ get_previous_words(int point, char **buffer, int *nwords)
 				inquotes = !inquotes;
 			if (!inquotes)
 			{
+				if (buf[start] == '\'')
+					in_single_quote = !in_single_quote;
 				if (buf[start] == ')')
 					parentheses++;
 				else if (buf[start] == '(')
@@ -5080,6 +5083,7 @@ get_previous_words(int point, char **buffer, int *nwords)
 						break;
 				}
 				else if (parentheses == 0 &&
+						 !in_single_quote &&
 						 strchr(WORD_BREAKS, buf[start - 1]))
 					break;
 			}
-- 
2.31.1.windows.1

#5Jacob Champion
pchampion@vmware.com
In reply to: tanghy.fnst@fujitsu.com (#4)
Re: [PATCH] support tab-completion for single quote input with equal sign

Hi Tang,

On Fri, 2021-09-03 at 04:32 +0000, tanghy.fnst@fujitsu.com wrote:

I'd appreciate it if you can share your test results with me.

Sure! Here's my output (after a `make clean && make`):

cd . && TESTDIR='/home/pchampion/workspace/postgres/src/bin/psql' PATH="/home/pchampion/workspace/postgres/tmp_install/usr/local/pgsql-master/bin:$PATH" LD_LIBRARY_PATH="/home/pchampion/workspace/postgres/tmp_install/usr/local/pgsql-master/lib" PGPORT='65432' PG_REGRESS='/home/pchampion/workspace/postgres/src/bin/psql/../../../src/test/regress/pg_regress' /usr/bin/prove -I ../../../src/test/perl/ -I . t/*.pl
t/010_tab_completion.pl .. 17/?
# Failed test 'tab-completion after single quoted text input with equal sign'
# at t/010_tab_completion.pl line 198.
# Actual output was "CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \a"
# Did not match "(?^:PUBLICATION)"
# Looks like you failed 1 test of 23.
t/010_tab_completion.pl .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/23 subtests
t/020_cancel.pl .......... ok

Test Summary Report
-------------------
t/010_tab_completion.pl (Wstat: 256 Tests: 23 Failed: 1)
Failed test: 17
Non-zero exit status: 1
Files=2, Tests=25, 8 wallclock secs ( 0.02 usr 0.01 sys + 1.48 cusr 0.37 csys = 1.88 CPU)
Result: FAIL
make: *** [Makefile:87: check] Error 1

Thanks,
--Jacob

#6Tom Lane
tgl@sss.pgh.pa.us
In reply to: tanghy.fnst@fujitsu.com (#4)
Re: [PATCH] support tab-completion for single quote input with equal sign

"tanghy.fnst@fujitsu.com" <tanghy.fnst@fujitsu.com> writes:

[ v2-0001-support-tab-completion-for-single-quote-input-wit.patch ]

Surely this patch is completely wrong? It needs more thought about
the interaction with the existing logic for double quotes, ie single
quote inside double quotes is not special, nor the reverse; nor should
parentheses inside quotes be counted. It also needs to be aware of
backslashes in escape-style strings.

I kind of doubt that it's actually possible to parse string literals
correctly when working backward, as this function does. For starters,
you won't know whether the string starts with "E", so you won't know
whether backslashes are special. We've got away with backwards
parsing so far because the syntax rules for double-quoted strings are
so much simpler. But if you want to handle single quotes, I think
you'll have to start by rearranging the code to parse forward. That's
likely to be fairly ticklish, see the comment about

* backwards scan has some interesting but intentional properties
* concerning parenthesis handling.

I wish that whoever wrote that (which I think was me :-() had been
more explicit. But I think that the point is that we look for a place
that's at the same parenthesis nesting level as the completion point,
not necessarily one that's globally outside of any parens. That will
be messy to handle if we want to convert to scanning forwards from
the start of the string.

I kind of wonder if it isn't time to enlist the help of psqlscan.l
instead of doubling down on the idea that tab-complete.c should have
its own half-baked SQL lexer.

regards, tom lane

#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#6)
Re: [PATCH] support tab-completion for single quote input with equal sign

I wrote:

Surely this patch is completely wrong? It needs more thought about
the interaction with the existing logic for double quotes, ie single
quote inside double quotes is not special, nor the reverse; nor should
parentheses inside quotes be counted. It also needs to be aware of
backslashes in escape-style strings.

Actually ... those are just implementation details, and now that
I've thought about it a little more, I question the entire concept
of making single-quoted strings be single words in tab-complete's
view. I think it's quite intentional that we don't do that;
if we did, it'd forever foreclose the possibility of tab-completing
*within* strings. You don't have to look any further than CREATE
SUBSCRIPTION itself to see possible applications of that: someone
could wish that

CREATE SUBSCRIPTION my_sub CONNECTION 'db<TAB>

would complete with "name=", or that <TAB> right after the quote
would offer a list of connection keywords.

(More generally, I'm afraid that people are already relying on this
behavior in other contexts, and thus that the proposed patch could
break more use-cases than it fixes.)

So now I think that this approach should be rejected, and that the
right thing is to fix the CREATE SUBSCRIPTION completion rules
to allow more than one "word" between CONNECTION and PUBLICATION.

Another idea that might be useful is to treat the opening and
closing quotes themselves as separate "words", which'd give
the CREATE SUBSCRIPTION rules a bit more to go on about when to
offer PUBLICATION.

regards, tom lane

#8Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jacob Champion (#5)
Re: [PATCH] support tab-completion for single quote input with equal sign

Jacob Champion <pchampion@vmware.com> writes:

t/010_tab_completion.pl .. 17/?
# Failed test 'tab-completion after single quoted text input with equal sign'
# at t/010_tab_completion.pl line 198.
# Actual output was "CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \a"
# Did not match "(?^:PUBLICATION)"
# Looks like you failed 1 test of 23.

Independently of the concerns I raised, I'm wondering how come you
are getting different results. Which readline or libedit version
are you using, on what platform?

regards, tom lane

#9Jacob Champion
pchampion@vmware.com
In reply to: Tom Lane (#8)
Re: [PATCH] support tab-completion for single quote input with equal sign

On Sat, 2021-09-04 at 11:32 -0400, Tom Lane wrote:

Jacob Champion <pchampion@vmware.com> writes:

t/010_tab_completion.pl .. 17/?
# Failed test 'tab-completion after single quoted text input with equal sign'
# at t/010_tab_completion.pl line 198.
# Actual output was "CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \a"
# Did not match "(?^:PUBLICATION)"
# Looks like you failed 1 test of 23.

Independently of the concerns I raised, I'm wondering how come you
are getting different results. Which readline or libedit version
are you using, on what platform?

Now you have me worried...

- Ubuntu 20.04
- libedit, version 3.1-20191231-1

--Jacob

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jacob Champion (#9)
Re: [PATCH] support tab-completion for single quote input with equal sign

Jacob Champion <pchampion@vmware.com> writes:

On Sat, 2021-09-04 at 11:32 -0400, Tom Lane wrote:

Independently of the concerns I raised, I'm wondering how come you
are getting different results. Which readline or libedit version
are you using, on what platform?

Now you have me worried...
- Ubuntu 20.04
- libedit, version 3.1-20191231-1

We've had troubles with libedit before :-(, and that particular release
is known to be a bit buggy [1]http://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=54510.

I can reproduce a problem using HEAD (no patch needed) and Fedora 32's
libedit-3.1-32.20191231cvs.fc32.x86_64: if I type

create subscription s connection foo <TAB>

then it happily completes "PUBLICATION", but if I type

create subscription s connection 'foo' <TAB>

I just get beeps.

I do *not* see this misbehavior on a nearby FreeBSD 13.0 machine
with libedit 3.1.20210216,1 (which isn't even the latest version).
So it's a fixed-some-time-ago bug. Given the symptoms, I wonder
if it isn't closely related to the original complaint at [1]http://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=54510.

Anyway, the bottom line from our standpoint is going to be that
we can't put a test case like this one into the TAP test. I recall
that getting 010_tab_completion.pl to pass everywhere was a huge
headache at the outset, so this conclusion doesn't surprise me
much.

regards, tom lane

[1]: http://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=54510

#11Jacob Champion
pchampion@vmware.com
In reply to: Tom Lane (#10)
Re: [PATCH] support tab-completion for single quote input with equal sign

On Wed, 2021-09-08 at 17:08 -0400, Tom Lane wrote:

Jacob Champion <pchampion@vmware.com> writes:

On Sat, 2021-09-04 at 11:32 -0400, Tom Lane wrote:

Independently of the concerns I raised, I'm wondering how come you
are getting different results. Which readline or libedit version
are you using, on what platform?

Now you have me worried...
- Ubuntu 20.04
- libedit, version 3.1-20191231-1

We've had troubles with libedit before :-(, and that particular release
is known to be a bit buggy [1].

That's... unfortunate. Thanks for the info; I wonder what other tab-
completion bugs I've just accepted as "standard behavior".

--Jacob

#12tanghy.fnst@fujitsu.com
tanghy.fnst@fujitsu.com
In reply to: Tom Lane (#7)
1 attachment(s)
RE: [PATCH] support tab-completion for single quote input with equal sign

On Saturday, September 4, 2021 11:58 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Actually ... those are just implementation details, and now that
I've thought about it a little more, I question the entire concept
of making single-quoted strings be single words in tab-complete's
view. I think it's quite intentional that we don't do that;
if we did, it'd forever foreclose the possibility of tab-completing
*within* strings. You don't have to look any further than CREATE
SUBSCRIPTION itself to see possible applications of that: someone
could wish that

CREATE SUBSCRIPTION my_sub CONNECTION 'db<TAB>

would complete with "name=", or that <TAB> right after the quote
would offer a list of connection keywords.

(More generally, I'm afraid that people are already relying on this
behavior in other contexts, and thus that the proposed patch could
break more use-cases than it fixes.)

Agreed. Thanks for your comments.

So now I think that this approach should be rejected, and that the
right thing is to fix the CREATE SUBSCRIPTION completion rules
to allow more than one "word" between CONNECTION and PUBLICATION.

Another idea that might be useful is to treat the opening and
closing quotes themselves as separate "words", which'd give
the CREATE SUBSCRIPTION rules a bit more to go on about when to
offer PUBLICATION.

Treat the opening and closing quotes themselves as separate "words" may affect some current tap completion.
So I tried to fix the CREATE SUBSCRIPTION completion rules in the V3 patch.
The basic idea is to check the head words of the input text as "CREATE SUBSCRIPTION subname CONNECTION anystring",
then check to see if anystring ends with single quote. If all check passed, PUBLICATION will be auto-completed.

Tap tests(including the one added in V3) has been passed.

Regards,
Tang

Attachments:

v3-0001-support-tab-completion-for-CONNECTION-string-with.patchapplication/octet-stream; name=v3-0001-support-tab-completion-for-CONNECTION-string-with.patchDownload
From 1aa9986d144d11ad57b127e460a348e05e687d85 Mon Sep 17 00:00:00 2001
From: tanghy <tanghy.fnst@fujitsu.com>
Date: Wed, 15 Sep 2021 23:37:34 +0900
Subject: [PATCH v3] support tab-completion for CONNECTION string with equal
 sign


diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 8695d22545..5537006b11 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -194,6 +194,14 @@ check_completion(
 
 clear_query();
 
+# check tab-completion for CONNECTION string with equal sign.
+check_completion(
+	"CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \t",
+	qr|PUBLICATION|,
+	"tab-completion for CONNECTION string with equal sign");
+
+clear_query();
+
 # COPY requires quoting
 # note: broken versions of libedit want to backslash the closing quote;
 # not much we can do about that
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5cd5838668..ec42784fef 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2810,8 +2810,11 @@ psql_completion(const char *text, int start, int end)
 /* CREATE SUBSCRIPTION */
 	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny))
 		COMPLETE_WITH("CONNECTION");
-	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny))
-		COMPLETE_WITH("PUBLICATION");
+	else if ((HeadMatches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny)))
+	{
+		if (ends_with(prev_wd, '\''))
+			COMPLETE_WITH("PUBLICATION");
+	}
 	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION",
 					 MatchAny, "PUBLICATION"))
 	{
-- 
2.33.0.windows.2

#13Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Tom Lane (#6)
1 attachment(s)
Re: [PATCH] support tab-completion for single quote input with equal sign

At Sat, 04 Sep 2021 10:18:24 -0400, Tom Lane <tgl@sss.pgh.pa.us> wrote in

I kind of wonder if it isn't time to enlist the help of psqlscan.l
instead of doubling down on the idea that tab-complete.c should have
its own half-baked SQL lexer.

So, I played with this idea and came up with the attached WIP. The
test added by the original patch succeeds with it not tweakig the
"Matches" part in psql_completion.

While I checked this I found several dubious parts in TAP test.

=== 010_tab_completion.pl
# COPY requires quoting
# note: broken versions of libedit want to backslash the closing quote;
# not much we can do about that
check_completion(
- "COPY foo FROM tmp_check/some\t",
+ "COPY foo FROM \'tmp_check/some\t",

The original command syntax is just wrong and this patch make
completion code treat the command line correctly (breaks the
"filename" into "tmp_check" "/" "some") and the test item fails.

check_completion(
-	"COPY foo FROM tmp_check/af\t",
-	qr|'tmp_check/afile|,
+	"COPY foo FROM \'tmp_check/af\t",
+	qr|'tmp_check/af\a?ile|,	# \a is BEL

This test fails for the same reason, but after fixing it the result
contains \a (BEL) in the output on my CentOS8. I'm not sure what is
happening here..

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

Attachments:

WIP_psql_completion_using_scanner.patchtext/x-patch; charset=us-asciiDownload
diff --git a/src/bin/psql/psqlscanslash.l b/src/bin/psql/psqlscanslash.l
index 51aa33e161..5d0d625809 100644
--- a/src/bin/psql/psqlscanslash.l
+++ b/src/bin/psql/psqlscanslash.l
@@ -149,11 +149,15 @@ other			.
 
 {space}|"\\"	{
 					yyless(0);
+					psqlscan_mark_whitespace(cur_state, true);
 					cur_state->start_state = YY_START;
 					return LEXRES_OK;
 				}
 
-{other}			{ ECHO; }
+{other}			{
+					psqlscan_mark_whitespace(cur_state, false);
+					ECHO;
+				}
 
 }
 
@@ -164,9 +168,10 @@ other			.
 	 * check for it here.
 	 */
 
-{space}+		{ }
+{space}+		{  psqlscan_mark_whitespace(cur_state, true); }
 
 "|"				{
+					psqlscan_mark_whitespace(cur_state, false);
 					if (option_type == OT_FILEPIPE)
 					{
 						/* treat like whole-string case */
@@ -182,6 +187,7 @@ other			.
 				}
 
 {other}			{
+					psqlscan_mark_whitespace(cur_state, false);
 					yyless(0);
 					BEGIN(xslasharg);
 				}
@@ -207,18 +213,21 @@ other			.
 					 * processing that encourages use of backslashes is rather
 					 * broken.
 					 */
+					psqlscan_mark_whitespace(cur_state, true);
 					yyless(0);
 					cur_state->start_state = YY_START;
 					return LEXRES_OK;
 				}
 
 {quote}			{
+					psqlscan_mark_whitespace(cur_state, false);
 					*option_quote = '\'';
 					unquoted_option_chars = 0;
 					BEGIN(xslashquote);
 				}
 
 "`"				{
+					psqlscan_mark_whitespace(cur_state, false);
 					backtick_start_offset = output_buf->len;
 					*option_quote = '`';
 					unquoted_option_chars = 0;
@@ -226,6 +235,7 @@ other			.
 				}
 
 {dquote}		{
+					psqlscan_mark_whitespace(cur_state, false);
 					ECHO;
 					*option_quote = '"';
 					unquoted_option_chars = 0;
@@ -233,6 +243,7 @@ other			.
 				}
 
 :{variable_char}+	{
+					psqlscan_mark_whitespace(cur_state, false);
 					/* Possible psql variable substitution */
 					if (cur_state->callbacks->get_variable == NULL)
 						ECHO;
@@ -278,6 +289,7 @@ other			.
 
 
 :\"{variable_char}+\"	{
+					psqlscan_mark_whitespace(cur_state, false);
 					psqlscan_escape_variable(cur_state, yytext, yyleng,
 											 PQUOTE_SQL_IDENT);
 					*option_quote = ':';
@@ -285,6 +297,7 @@ other			.
 				}
 
 :\{\?{variable_char}+\}	{
+					psqlscan_mark_whitespace(cur_state, false);
 					psqlscan_test_variable(cur_state, yytext, yyleng);
 				}
 
@@ -317,6 +330,7 @@ other			.
 				}
 
 {other}			{
+					psqlscan_mark_whitespace(cur_state, false);
 					unquoted_option_chars++;
 					ECHO;
 				}
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 8695d22545..80181d1355 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -194,19 +194,27 @@ check_completion(
 
 clear_query();
 
+# check tab-completion for CONNECTION string with equal sign.
+check_completion(
+	"CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \t",
+	qr|PUBLICATION|,
+	"tab-completion for CONNECTION string with equal sign");
+
+clear_query();
+
 # COPY requires quoting
 # note: broken versions of libedit want to backslash the closing quote;
 # not much we can do about that
 check_completion(
-	"COPY foo FROM tmp_check/some\t",
+	"COPY foo FROM \'tmp_check/some\t",
 	qr|'tmp_check/somefile\\?' |,
 	"quoted filename completion with one possibility");
 
 clear_line();
 
 check_completion(
-	"COPY foo FROM tmp_check/af\t",
-	qr|'tmp_check/afile|,
+	"COPY foo FROM \'tmp_check/af\t",
+	qr|'tmp_check/af\a?ile|,	# \a is BEL
 	"quoted filename completion with multiple possibilities");
 
 # some versions of readline/libedit require two tabs here, some only need one
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5cd5838668..86871bd0a2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -50,6 +50,7 @@
 #include "pqexpbuffer.h"
 #include "settings.h"
 #include "stringutils.h"
+#include "psqlscanslash.h"
 
 /*
  * Ancient versions of libedit provide filename_completion_function()
@@ -4983,15 +4984,24 @@ exec_query(const char *query)
  * Words are returned right to left, that is, previous_words[0] gets the last
  * word before point, previous_words[1] the next-to-last, etc.
  */
+static const PsqlScanCallbacks psqlscan_callbacks = {
+	NULL,
+};
 static char **
 get_previous_words(int point, char **buffer, int *nwords)
 {
 	char	  **previous_words;
 	char	   *buf;
 	char	   *outptr;
+	char	   *endptr;
 	int			words_found = 0;
 	int			i;
+	PsqlScanState scan_state;
+	PsqlScanToken tok;
+	PQExpBufferData query_buf;
 
+	initPQExpBuffer(&query_buf);
+	
 	/*
 	 * If we have anything in tab_completion_query_buf, paste it together with
 	 * rl_line_buffer to construct the full query.  Otherwise we can just use
@@ -5021,81 +5031,61 @@ get_previous_words(int point, char **buffer, int *nwords)
 	 */
 	previous_words = (char **) pg_malloc(point * sizeof(char *));
 	*buffer = outptr = (char *) pg_malloc(point * 2);
+	endptr = outptr + point * 2;	/* limit + 1 */
 
-	/*
-	 * First we look for a non-word char before the current point.  (This is
-	 * probably useless, if readline is on the same page as we are about what
-	 * is a word, but if so it's cheap.)
-	 */
-	for (i = point - 1; i >= 0; i--)
+	scan_state = psql_scan_create(&psqlscan_callbacks);
+	psql_scan_setup(scan_state, buf, strlen(buf),
+					pset.encoding, standard_strings());
+	if (psql_scan(scan_state, NULL,  NULL) != PSCAN_BACKSLASH)
 	{
-		if (strchr(WORD_BREAKS, buf[i]))
-			break;
-	}
-	point = i;
+		words_found = psql_scan_get_ntokens(scan_state);
 
-	/*
-	 * Now parse words, working backwards, until we hit start of line.  The
-	 * backwards scan has some interesting but intentional properties
-	 * concerning parenthesis handling.
-	 */
-	while (point >= 0)
-	{
-		int			start,
-					end;
-		bool		inquotes = false;
-		int			parentheses = 0;
+		if (words_found > 0 && !psql_scan_last_token_is_whitespace(scan_state))
+			words_found--;
 
-		/* now find the first non-space which then constitutes the end */
-		end = -1;
-		for (i = point; i >= 0; i--)
+		tok = NULL;
+		for (i = words_found - 1 ; i >= 0 ; i--)
 		{
-			if (!isspace((unsigned char) buf[i]))
-			{
-				end = i;
-				break;
-			}
+			tok = psql_scan_get_next_token(scan_state, tok);
+			previous_words[i] = outptr;
+			outptr += psql_scan_get_token_string(scan_state, tok, outptr);
 		}
-		/* if no end found, we're done */
-		if (end < 0)
-			break;
+		psql_scan_destroy(scan_state);
+	}
+	else
+	{
+		char *word = psql_scan_slash_command(scan_state);
+		words_found = 0;
 
-		/*
-		 * Otherwise we now look for the start.  The start is either the last
-		 * character before any word-break character going backwards from the
-		 * end, or it's simply character 0.  We also handle open quotes and
-		 * parentheses.
-		 */
-		for (start = end; start > 0; start--)
+		if (word && psql_scan_last_token_is_whitespace(scan_state))
 		{
-			if (buf[start] == '"')
-				inquotes = !inquotes;
-			if (!inquotes)
+			int  n = point - 1;
+
+			previous_words[n--] = outptr;
+			*outptr++ = '\\';
+			strcpy(outptr, word);
+			free(word);
+			outptr += strlen(outptr) + 1;
+			words_found++;
+
+			while ((word = psql_scan_slash_option(scan_state, OT_NORMAL,
+												  NULL, false)) != NULL &&
+				   psql_scan_last_token_is_whitespace(scan_state))
 			{
-				if (buf[start] == ')')
-					parentheses++;
-				else if (buf[start] == '(')
-				{
-					if (--parentheses <= 0)
-						break;
-				}
-				else if (parentheses == 0 &&
-						 strchr(WORD_BREAKS, buf[start - 1]))
-					break;
+				previous_words[n--] = outptr;
+				strcpy(outptr, word);
+				free(word);
+				outptr += strlen(outptr) + 1;
+				words_found++;
 			}
-		}
 
-		/* Return the word located at start to end inclusive */
-		previous_words[words_found++] = outptr;
-		i = end - start + 1;
-		memcpy(outptr, &buf[start], i);
-		outptr += i;
-		*outptr++ = '\0';
-
-		/* Continue searching */
-		point = start - 1;
+			memcpy(previous_words, previous_words + n + 1,
+				   words_found * sizeof(char *));
+		}
 	}
 
+	Assert (point == 0 || outptr < endptr);
+
 	/* Release parsing input workspace, if we made one above */
 	if (buf != rl_line_buffer)
 		free(buf);
@@ -5104,6 +5094,7 @@ get_previous_words(int point, char **buffer, int *nwords)
 	return previous_words;
 }
 
+
 /*
  * Look up the type for the GUC variable with the passed name.
  *
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index 0fab48a382..4225c50efd 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -381,8 +381,10 @@ other			.
 					 * which is pretty dubious but is the historical
 					 * behavior.
 					 */
-					if (!(output_buf->len == 0 || yytext[0] == '-'))
+					if (output_buf &&
+						!(output_buf->len == 0 || yytext[0] == '-'))
 						ECHO;
+					psqlscan_mark_whitespace(cur_state, true);
 				}
 
 {xcstart}		{
@@ -414,6 +416,7 @@ other			.
 				}
 
 {op_chars}		{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_OP);
 					ECHO;
 				}
 
@@ -424,10 +427,13 @@ other			.
 
 {xbstart}		{
 					BEGIN(xb);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 <xh>{xhinside}	|
 <xb>{xbinside}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 
@@ -439,11 +445,14 @@ other			.
 					 * to mark it for the input routine as a hex string.
 					 */
 					BEGIN(xh);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 
 {xnstart}		{
 					yyless(1);	/* eat only 'n' this time */
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 
@@ -452,14 +461,20 @@ other			.
 						BEGIN(xq);
 					else
 						BEGIN(xe);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 {xestart}		{
 					BEGIN(xe);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 {xusstart}		{
 					BEGIN(xus);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 
@@ -474,6 +489,7 @@ other			.
 					 */
 					cur_state->state_before_str_stop = YYSTATE;
 					BEGIN(xqs);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xqs>{quotecontinue} {
@@ -483,6 +499,7 @@ other			.
 					 * added to the literal's contents.
 					 */
 					BEGIN(cur_state->state_before_str_stop);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xqs>{quotecontinuefail} |
@@ -494,46 +511,62 @@ other			.
 					 */
 					yyless(0);
 					BEGIN(INITIAL);
+
+					if (cur_state->quote_depth > 0)
+						cur_state->quote_depth--;
+					/* terminate this string */
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					/* There's nothing to echo ... */
 				}
 
 <xq,xe,xus>{xqdouble} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_STRING);
 					ECHO;
 				}
 <xq,xus>{xqinside}  {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xeinside}  {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xeunicode} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xeunicodefail}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xeescape}  {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xeoctesc}  {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>{xehexesc}  {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xe>.			{
 					/* This is only needed for \ just before EOF */
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 
 {dolqdelim}		{
 					cur_state->dolqstart = pg_strdup(yytext);
 					BEGIN(xdolq);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {dolqfailed}	{
 					/* throw back all but the initial "$" */
 					yyless(1);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 <xdolq>{dolqdelim} {
@@ -552,77 +585,102 @@ other			.
 						 */
 						yyless(yyleng - 1);
 					}
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 <xdolq>{dolqinside} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 <xdolq>{dolqfailed} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 <xdolq>.		{
 					/* This is only needed for $ inside the quoted text */
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {xdstart}		{
 					BEGIN(xd);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_IDENTIFIER);
 					ECHO;
 				}
 {xuistart}		{
 					BEGIN(xui);
+					cur_state->quote_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_IDENTIFIER);
 					ECHO;
 				}
 <xd>{xdstop}	{
 					BEGIN(INITIAL);
+					if (cur_state->quote_depth > 0)
+						cur_state->quote_depth--;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xui>{dquote}	{
 					BEGIN(INITIAL);
+					if (cur_state->quote_depth > 0)
+						cur_state->quote_depth--;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xd,xui>{xddouble}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 <xd,xui>{xdinside}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_CONT);
 					ECHO;
 				}
 
 {xufailed}	{
 					/* throw back all but the initial u/U */
 					yyless(1);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {typecast}		{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {dot_dot}		{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {colon_equals}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {equals_greater} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {less_equals}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {greater_equals} {
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {less_greater}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {not_equals}	{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
@@ -634,16 +692,19 @@ other			.
 
 "("				{
 					cur_state->paren_depth++;
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_LPAREN);
 					ECHO;
 				}
 
 ")"				{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_RPAREN);
 					if (cur_state->paren_depth > 0)
 						cur_state->paren_depth--;
 					ECHO;
 				}
 
 ";"				{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 					if (cur_state->paren_depth == 0 && cur_state->begin_depth == 0)
 					{
@@ -765,6 +826,7 @@ other			.
 	 */
 
 {self}			{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
@@ -832,25 +894,31 @@ other			.
 						/* Strip the unwanted chars from the token */
 						yyless(nchars);
 					}
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_OP);
 					ECHO;
 				}
 
 {param}			{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {integer}		{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {decimal}		{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {decimalfail}	{
 					/* throw back the .., and treat as integer */
 					yyless(yyleng - 2);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {real}			{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {realfail1}		{
@@ -860,11 +928,13 @@ other			.
 					 * (in psql, we don't actually care...)
 					 */
 					yyless(yyleng - 1);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 {realfail2}		{
 					/* throw back the [Ee][+-], and proceed as above */
 					yyless(yyleng - 2);
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
@@ -919,10 +989,12 @@ other			.
 						}
 					}
 
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
 {other}			{
+					psqlscan_add_token(cur_state, yytext, yyleng, PSCAN_TOKEN_WORD);
 					ECHO;
 				}
 
@@ -1036,6 +1108,134 @@ psql_scan_setup(PsqlScanState state,
 	state->refline = state->scanline;
 }
 
+/*
+ * Add a token to token array.
+ *
+ * This is similar to psqlscan_emit but instead adds tokens into an array that
+ * is to be used for syntax completion. Quoted identifiers and strings are
+ * merged together as a token.
+ */
+void
+psqlscan_add_token(PsqlScanState state, const char *txt, int len,
+					PsqlScanTokenType type)
+{
+	PsqlScanToken tok;
+	PsqlScanToken prev;
+
+	if (state->output_buf)
+		return;
+
+	/* we have met non-whitespace characters */
+	state->last_is_whitespace = false;
+
+	/* merge this token into the previous one if it is continued */
+	if (state->token_continue)
+	{
+		Assert (state->lasttoken);
+		state->lasttoken->len += len;
+
+		/* still continue to the next token? */
+		state->token_continue = (state->quote_depth > 0);
+
+		/* 
+		 * If this token has finished, assume it is followed by whitespace.
+		 * (Maybe this is not needed since readline requires a whitespace.)
+		 */
+		if (!state->token_continue)
+			state->last_is_whitespace = true;
+		
+		return;
+	}
+
+	/* Append new token */
+	prev = state->lasttoken;
+	tok = pg_malloc(sizeof(PsqlScanToken));
+
+	/*
+	 * This logic could be simplified if we stored the tokens in the reverse
+	 * order and it is convenient for tab-completion code, but we choose to
+	 * store in the saner order here.
+	 */
+	if (prev)
+		prev->next = tok;
+	else
+		state->tokenlist = tok;
+
+	state->lasttoken = tok;
+
+	/* if this token starts quote, it continues to the following tokens */
+	state->token_continue = (state->quote_depth > 0);
+	tok->type = type;
+	tok->startpos = txt - state->scanbuf;
+	tok->len = len;
+	tok->paren_depth = state->paren_depth;
+	tok->next = NULL;
+}
+
+/*
+ * Set if the last token was whitespace.
+ */
+void
+
+psqlscan_mark_whitespace(PsqlScanState state, bool is_ws)
+{
+	state->last_is_whitespace = is_ws;
+}
+
+
+/*
+ * Returns the next token.  prev=NULL means requesting for the first token.
+ */
+const PsqlScanToken
+psql_scan_get_next_token(PsqlScanState state, const PsqlScanToken prev)
+{
+	if (!prev)
+		return state->tokenlist;
+
+	return prev->next;
+}
+
+/*
+ * Copy token string into given buffer then return the consumed buffer size
+ * including the terminating zero byte.
+ */
+int
+psql_scan_get_token_string(PsqlScanState state,
+						   const PsqlScanToken tok, char *buf)
+{
+	memcpy(buf, &state->scanline[tok->startpos], tok->len);
+	buf[tok->len] = 0;
+
+	return tok->len + 1;
+}
+
+/*
+ * Returns the number of tokens.
+ */
+int
+psql_scan_get_ntokens(PsqlScanState state)
+{
+	int i = 0;
+	PsqlScanToken tok = state->tokenlist;
+	
+	while (tok)
+	{
+		i++;
+		tok = tok->next;
+	}
+
+	return i;
+}
+
+/*
+ *
+ */
+bool
+psql_scan_last_token_is_whitespace(PsqlScanState state)
+{
+	return state->last_is_whitespace;
+}
+
 /*
  * Do lexical analysis of SQL command text.
  *
@@ -1075,7 +1275,8 @@ psql_scan(PsqlScanState state,
 {
 	PsqlScanResult result;
 	int			lexresult;
-
+	promptStatus_t	tprompt;
+	
 	/* Must be scanning already */
 	Assert(state->scanbufhandle != NULL);
 
@@ -1104,60 +1305,60 @@ psql_scan(PsqlScanState state,
 					if (state->paren_depth > 0)
 					{
 						result = PSCAN_INCOMPLETE;
-						*prompt = PROMPT_PAREN;
+						tprompt = PROMPT_PAREN;
 					}
 					else if (state->begin_depth > 0)
 					{
 						result = PSCAN_INCOMPLETE;
-						*prompt = PROMPT_CONTINUE;
+						tprompt = PROMPT_CONTINUE;
 					}
-					else if (query_buf->len > 0)
+					else if (query_buf ? query_buf->len > 0 : state->tokenlist != NULL)
 					{
 						result = PSCAN_EOL;
-						*prompt = PROMPT_CONTINUE;
+						tprompt = PROMPT_CONTINUE;
 					}
 					else
 					{
 						/* never bother to send an empty buffer */
 						result = PSCAN_INCOMPLETE;
-						*prompt = PROMPT_READY;
+						tprompt = PROMPT_READY;
 					}
 					break;
 				case xb:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_SINGLEQUOTE;
+					tprompt = PROMPT_SINGLEQUOTE;
 					break;
 				case xc:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_COMMENT;
+					tprompt = PROMPT_COMMENT;
 					break;
 				case xd:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_DOUBLEQUOTE;
+					tprompt = PROMPT_DOUBLEQUOTE;
 					break;
 				case xh:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_SINGLEQUOTE;
+					tprompt = PROMPT_SINGLEQUOTE;
 					break;
 				case xe:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_SINGLEQUOTE;
+					tprompt = PROMPT_SINGLEQUOTE;
 					break;
 				case xq:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_SINGLEQUOTE;
+					tprompt = PROMPT_SINGLEQUOTE;
 					break;
 				case xdolq:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_DOLLARQUOTE;
+					tprompt = PROMPT_DOLLARQUOTE;
 					break;
 				case xui:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_DOUBLEQUOTE;
+					tprompt = PROMPT_DOUBLEQUOTE;
 					break;
 				case xus:
 					result = PSCAN_INCOMPLETE;
-					*prompt = PROMPT_SINGLEQUOTE;
+					tprompt = PROMPT_SINGLEQUOTE;
 					break;
 				default:
 					/* can't get here */
@@ -1167,11 +1368,11 @@ psql_scan(PsqlScanState state,
 			break;
 		case LEXRES_SEMI:		/* semicolon */
 			result = PSCAN_SEMICOLON;
-			*prompt = PROMPT_READY;
+			tprompt = PROMPT_READY;
 			break;
 		case LEXRES_BACKSLASH:	/* backslash */
 			result = PSCAN_BACKSLASH;
-			*prompt = PROMPT_READY;
+			tprompt = PROMPT_READY;
 			break;
 		default:
 			/* can't get here */
@@ -1179,6 +1380,8 @@ psql_scan(PsqlScanState state,
 			exit(1);
 	}
 
+	if (prompt)
+		*prompt = tprompt;
 	return result;
 }
 
@@ -1221,14 +1424,28 @@ psql_scan_finish(PsqlScanState state)
 void
 psql_scan_reset(PsqlScanState state)
 {
+	PsqlScanToken tok;
+
 	state->start_state = INITIAL;
 	state->paren_depth = 0;
+	state->quote_depth = 0;
 	state->xcdepth = 0;			/* not really necessary */
 	if (state->dolqstart)
 		free(state->dolqstart);
 	state->dolqstart = NULL;
 	state->identifier_count = 0;
 	state->begin_depth = 0;
+	state->token_continue = false;
+
+	/* free up toekn list */
+	tok = state->tokenlist;
+	while (tok)
+	{
+		PsqlScanToken prev = tok;
+		tok = tok->next;
+		free(prev);
+	}
+	state->tokenlist = NULL;
 }
 
 /*
@@ -1419,6 +1636,9 @@ psqlscan_emit(PsqlScanState state, const char *txt, int len)
 {
 	PQExpBuffer output_buf = state->output_buf;
 
+	if (!output_buf)
+		return;
+
 	if (state->safe_encoding)
 		appendBinaryPQExpBuffer(output_buf, txt, len);
 	else
diff --git a/src/include/fe_utils/psqlscan.h b/src/include/fe_utils/psqlscan.h
index e55f1fa213..5f32106f3b 100644
--- a/src/include/fe_utils/psqlscan.h
+++ b/src/include/fe_utils/psqlscan.h
@@ -57,6 +57,20 @@ typedef enum
 	PQUOTE_SHELL_ARG			/* quote if needed to be safe in a shell cmd */
 } PsqlScanQuoteType;
 
+typedef enum PsqlScanTokenType
+{
+	PSCAN_TOKEN_WORD,
+	PSCAN_TOKEN_IDENTIFIER,
+	PSCAN_TOKEN_LPAREN,
+	PSCAN_TOKEN_RPAREN,
+	PSCAN_TOKEN_STRING,
+	PSCAN_TOKEN_OP,
+	PSCAN_TOKEN_CONT
+} PsqlScanTokenType;
+
+struct PsqlScanTokenData;
+typedef struct PsqlScanTokenData *PsqlScanToken;
+
 /* Callback functions to be used by the lexer */
 typedef struct PsqlScanCallbacks
 {
@@ -76,7 +90,12 @@ extern void psql_scan_setup(PsqlScanState state,
 							const char *line, int line_len,
 							int encoding, bool std_strings);
 extern void psql_scan_finish(PsqlScanState state);
-
+extern const PsqlScanToken psql_scan_get_next_token(PsqlScanState state,
+													const PsqlScanToken prev);
+extern int psql_scan_get_token_string(PsqlScanState state,
+									  const PsqlScanToken tok, char *buf);
+extern int	psql_scan_get_ntokens(PsqlScanState state);
+extern bool psql_scan_last_token_is_whitespace(PsqlScanState state);
 extern PsqlScanResult psql_scan(PsqlScanState state,
 								PQExpBuffer query_buf,
 								promptStatus_t *prompt);
diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h
index 8ada977092..3fc6e4d97e 100644
--- a/src/include/fe_utils/psqlscan_int.h
+++ b/src/include/fe_utils/psqlscan_int.h
@@ -75,6 +75,19 @@ typedef struct StackElem
 	struct StackElem *next;
 } StackElem;
 
+/*
+ * psql_scan has another mode that stores tokenized data instead of a
+ * cleaned-up string.  This represents every token in the token array.
+ */
+typedef struct PsqlScanTokenData
+{
+	PsqlScanTokenType	type;		/* toke type */
+	int					startpos;	/* start position in state.scanline */
+	int					len;		/* token length */
+	int					paren_depth; /* depth of parenthesized region */
+	struct PsqlScanTokenData *next;
+} PsqlScanTokenData;
+
 /*
  * All working state of the lexer must be stored in PsqlScanStateData
  * between calls.  This allows us to have multiple open lexer operations,
@@ -112,6 +125,7 @@ typedef struct PsqlScanStateData
 	int			start_state;	/* yylex's starting/finishing state */
 	int			state_before_str_stop;	/* start cond. before end quote */
 	int			paren_depth;	/* depth of nesting in parentheses */
+	int			quote_depth;
 	int			xcdepth;		/* depth of nesting in slash-star comments */
 	char	   *dolqstart;		/* current $foo$ quote start string */
 
@@ -123,6 +137,11 @@ typedef struct PsqlScanStateData
 	char		identifiers[4]; /* records the first few identifiers */
 	int			begin_depth;	/* depth of begin/end pairs */
 
+	PsqlScanToken	tokenlist;		/* list of tokens*/
+	PsqlScanToken	lasttoken;		/* last token for fast access */
+	bool			token_continue;	/* the last token is continuing */
+	bool		last_is_whitespace;	/* last token was whitespace */
+
 	/*
 	 * Callback functions provided by the program making use of the lexer,
 	 * plus a void* callback passthrough argument.
@@ -146,6 +165,9 @@ extern YY_BUFFER_STATE psqlscan_prepare_buffer(PsqlScanState state,
 											   const char *txt, int len,
 											   char **txtcopy);
 extern void psqlscan_emit(PsqlScanState state, const char *txt, int len);
+extern void psqlscan_add_token(PsqlScanState state, const char *txt, int len,
+	PsqlScanTokenType type);
+extern void psqlscan_mark_whitespace(PsqlScanState state, bool is_ws);
 extern char *psqlscan_extract_substring(PsqlScanState state,
 										const char *txt, int len);
 extern void psqlscan_escape_variable(PsqlScanState state,
#14Michael Paquier
michael@paquier.xyz
In reply to: Kyotaro Horiguchi (#13)
Re: [PATCH] support tab-completion for single quote input with equal sign

On Fri, Sep 17, 2021 at 02:45:57AM +0900, Kyotaro Horiguchi wrote:

This test fails for the same reason, but after fixing it the result
contains \a (BEL) in the output on my CentOS8. I'm not sure what is
happening here..

The patch is still failing under the CF bot, and this last update was
two months ago. I am marking it as returned with feedback.
--
Michael

#15Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Michael Paquier (#14)
Re: [PATCH] support tab-completion for single quote input with equal sign

At Fri, 3 Dec 2021 15:16:55 +0900, Michael Paquier <michael@paquier.xyz> wrote in

On Fri, Sep 17, 2021 at 02:45:57AM +0900, Kyotaro Horiguchi wrote:

This test fails for the same reason, but after fixing it the result
contains \a (BEL) in the output on my CentOS8. I'm not sure what is
happening here..

The patch is still failing under the CF bot, and this last update was
two months ago. I am marking it as returned with feedback.

I thought the last *WIP* patch as just a proposal of another direction
after the first attempt was failed.

I'll start another thread named as 'let's psql-completion share the
psql command line lexer' or such, after rebasing and some polishing.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#16torikoshia
torikoshia@oss.nttdata.com
In reply to: Kyotaro Horiguchi (#15)
1 attachment(s)
Re: [PATCH] support tab-completion for single quote input with equal sign

On Thursday, July 22, 2021 1:05 PM, tanghy(dot)fnst(at)fujitsu(dot)com
<tanghy(dot)fnst(at)fujitsu(dot)com> wrote

I found a problem when using tab-completion as follows:

CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=localhost port=5432 dbname=postgres' [TAB]

The word 'PUBLICATION' couldn't be auto completed as expected.

I too wondered about this behavior.

v3-0001-support-tab-completion-for-CONNECTION-string-with.patch

I applied the patch and succeeded in the above case, but failed in the
below case.

=# CREATE SUBSCRIPTION s1 CONNECTION 'a=' PUBLICATION p1 <tab>

Before applying the patch, 'WITH (' was completed, but now it completes
nothing since it matches the below condition:

18 + else if ((HeadMatches("CREATE", "SUBSCRIPTION", MatchAny,
"CONNECTION", MatchAny)))
19 + {

I updated the patch going along with the v3 direction.

What do you think?

--
Regards,

--
Atsushi Torikoshi
NTT DATA CORPORATION

Attachments:

v4-0001-support-tab-completion-for-CONNECTION-string-with.patchtext/x-diff; name=v4-0001-support-tab-completion-for-CONNECTION-string-with.patchDownload
From 0e2612a1c7762b64357c85ce04e62b5ba0cdb4f7 Mon Sep 17 00:00:00 2001
From: Atsushi Torikoshi <torikoshia@oss.nttdata.com>
Date: Tue, 10 Jan 2023 09:51:30 +0900
Subject: [PATCH v4] Support tab-completion after conninfo using '='

Previously, tab-completion after CONNECTION 'conninfo' in CREATE
SUBSCRIPTION didn't work when conninfo contained '=' since conninfo
was considered multiple words.

---
 src/bin/psql/t/010_tab_completion.pl |  8 ++++++++
 src/bin/psql/tab-complete.c          | 12 +++++++++---
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 7746c75e0c..4a1e8c8c32 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -288,6 +288,14 @@ check_completion(
 # broken versions of libedit require clear_line not clear_query here
 clear_line();
 
+# check tab-completion for CONNECTION string with equal sign.
+check_completion(
+   "CREATE SUBSCRIPTION my_sub CONNECTION 'host=localhost port=5432 dbname=postgres' \t",
+   qr|PUBLICATION|,
+   "tab-completion for CONNECTION string with equal sign");
+
+clear_line();
+
 # COPY requires quoting
 # note: broken versions of libedit want to backslash the closing quote;
 # not much we can do about that
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 23750ea5fb..dd66f1c7a2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3235,10 +3235,16 @@ psql_completion(const char *text, int start, int end)
 /* CREATE SUBSCRIPTION */
 	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny))
 		COMPLETE_WITH("CONNECTION");
-	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny))
+	/*
+		conninfo is considered as multiple words because it contains '='
+		and it makes conninfo doesn't match MatchAny.
+		Here we match conninfo noting that it ends with a single quotation.
+	*/
+	else if ((HeadMatches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION",
+					 MatchAny)) && ends_with(prev_wd, '\''))
 		COMPLETE_WITH("PUBLICATION");
-	else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION",
-					 MatchAny, "PUBLICATION"))
+	else if ((HeadMatches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION",
+					 MatchAny)) && TailMatches("PUBLICATION"))
 	{
 		/* complete with nothing here as this refers to remote publications */
 	}

base-commit: 57d11ef028d126f95595c08c62ffb4c5147d0f86
-- 
2.25.1

#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: torikoshia (#16)
Re: [PATCH] support tab-completion for single quote input with equal sign

torikoshia <torikoshia@oss.nttdata.com> writes:

I updated the patch going along with the v3 direction.

I think this adds about as many failure modes as it removes,
if not more.

* The connection string doesn't necessarily end with "'"; it could
be a dollar-quoted string.

* If it is a dollar-quoted string, there could be "'" marks internal
to it, allowing PUBLICATION to be falsely offered when we're really
still within the connection string.

* The following WITH options could contain "'", allowing PUBLICATION
to be falsely offered within that clause.

I've spent some effort previously on getting tab-completion to deal
sanely with single-quoted strings, but everything I've tried has
crashed and burned :-(, mainly because it's not clear when to take
the whole literal as one "word" and when not. A relevant example
here is that somebody might wish that we could tab-complete within
the connection string, e.g. that

CREATE SUBSCRIPTION sub CONNECTION 'db<TAB>

would complete with "name=". We have the info available from libpq
to do that, if only we could figure out when to apply it. I think
we need some pretty fundamental design work to figure out what we
want to do in this area, and that in the meantime putting band-aids
on specific cases is probably not very productive.

regards, tom lane

#18Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#17)
Re: [PATCH] support tab-completion for single quote input with equal sign

I wrote:

I've spent some effort previously on getting tab-completion to deal
sanely with single-quoted strings, but everything I've tried has
crashed and burned :-(, mainly because it's not clear when to take
the whole literal as one "word" and when not.

After a little further thought, a new idea occurred to me: maybe
we could push some of the problem down into the Matches functions.
Consider inventing a couple of new match primitives:

* MatchLiteral matches one or more parse tokens that form a single
complete, valid SQL literal string (either single-quoted or dollar
style). Use it like

else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchLiteral))
COMPLETE_WITH("PUBLICATION");

I think there's no question that most Matches calls that might subsume
a quoted literal would prefer to treat the literal as a single word,
and this'd let them do that correctly.

* MatchLiteralBegin matches the opening of a literal string (either '
or $...$). Handwaving freely, we might do

else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchLiteralBegin))
COMPLETE_WITH(List_of_connection_keywords);

This part of the idea still needs some thought, because it remains
unclear how we might offer completion for connection keywords
after the first one.

Implementing these primitives might be a little tricky too.
If memory serves, readline and libedit have different behaviors
around quote marks. But at least it seems like a framework
that could solve a number of problems, if we can make it go.

regards, tom lane

#19torikoshia
torikoshia@oss.nttdata.com
In reply to: Tom Lane (#18)
Re: [PATCH] support tab-completion for single quote input with equal sign

On 2023-01-11 11:28, Tom Lane wrote:

I wrote:

I've spent some effort previously on getting tab-completion to deal
sanely with single-quoted strings, but everything I've tried has
crashed and burned :-(, mainly because it's not clear when to take
the whole literal as one "word" and when not.

After a little further thought, a new idea occurred to me: maybe
we could push some of the problem down into the Matches functions.
Consider inventing a couple of new match primitives:

Thanks for the idea!
I'm going to try it.

--
Atsushi Torikoshi
NTT DATA CORPORATION