proposal - assign result of query to psql variable
Hello
we cannot actually store result of query to psql variable
I propose a new slash statement "\eset for this purpose
Syntax:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returned
Usage:
\eset var1, var2 select version(), current_date
Current workaround is not friendly and it is not usable for more than
one target variable
postgres=# \set myvar `psql -A -t -c "select version()" postgres `
postgres=# \echo :myvar
PostgreSQL 9.1.4 on x86_64-unknown-linux-gnu, compiled by gcc (GCC)
4.7.0 20120507 (Red Hat 4.7.0-5), 64-bit
Regards
Pavel
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returned
Better would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybe
select x,y,... from ...
\gset var1 var2 ...
regards, tom lane
2012/7/26 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returnedBetter would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybeselect x,y,... from ...
\gset var1 var2 ...
it could be
Pavel
Show quoted text
regards, tom lane
On Thu, Jul 26, 2012 at 01:36:17AM -0400, Tom Lane wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returnedBetter would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybeselect x,y,... from ...
\gset var1 var2 ...
How about
\gset var1,,,var2,var3...
The above shows how one would skip assigning variables in the target
list, which one might want to do.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
2012/7/26 David Fetter <david@fetter.org>:
On Thu, Jul 26, 2012 at 01:36:17AM -0400, Tom Lane wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returnedBetter would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybeselect x,y,... from ...
\gset var1 var2 ...How about
\gset var1,,,var2,var3...
I don't like this - you can use fake variable - and ignoring some
variable has no big effect on client
Pavel
Show quoted text
The above shows how one would skip assigning variables in the target
list, which one might want to do.Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.icsRemember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
On Thu, Jul 26, 2012 at 08:31:13AM +0200, Pavel Stehule wrote:
2012/7/26 David Fetter <david@fetter.org>:
On Thu, Jul 26, 2012 at 01:36:17AM -0400, Tom Lane wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returnedBetter would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybeselect x,y,... from ...
\gset var1 var2 ...How about
\gset var1,,,var2,var3...
I don't like this - you can use fake variable - and ignoring some
variable has no big effect on client
Why assign to a variable you'll never use?
Cheers,
David.
P.S. The bike shed should be puce with blaze orange pin-striping.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
2012/7/26 David Fetter <david@fetter.org>:
On Thu, Jul 26, 2012 at 08:31:13AM +0200, Pavel Stehule wrote:
2012/7/26 David Fetter <david@fetter.org>:
On Thu, Jul 26, 2012 at 01:36:17AM -0400, Tom Lane wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
\eset variable [, variable [..]] query -- it raise exception when
more than one row is returned or when no row is returnedBetter would be a variant on \g, that is you type in the query and
then tell it where to put the result. We have learned the hard way
that putting SQL commands into the arguments of backslash commands
is a horrid idea. Maybeselect x,y,... from ...
\gset var1 var2 ...How about
\gset var1,,,var2,var3...
I don't like this - you can use fake variable - and ignoring some
variable has no big effect on clientWhy assign to a variable you'll never use?
so why you get data from server, when you would not to use it ?
no offence, probably it is not hard to implement it - because we use
own parser, but I see this proposal little bit obscure
Tom - your proposal release of stored dataset just before next
statement, not like now on the end of statement?
Regards
Pavel
Show quoted text
Cheers,
David.P.S. The bike shed should be puce with blaze orange pin-striping.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.icsRemember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/7/26 David Fetter <david@fetter.org>:
How about
\gset var1,,,var2,var3...
I don't like this - you can use fake variable - and ignoring some
variable has no big effect on client
Why assign to a variable you'll never use?
so why you get data from server, when you would not to use it ?
Yeah. I don't see why you'd be likely to write a select that computes
columns you don't actually want.
Tom - your proposal release of stored dataset just before next
statement, not like now on the end of statement?
Huh? I think you'd assign the values to the variables and then PQclear
the result right away.
regards, tom lane
Hello
2012/7/27 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/7/26 David Fetter <david@fetter.org>:
How about
\gset var1,,,var2,var3...I don't like this - you can use fake variable - and ignoring some
variable has no big effect on clientWhy assign to a variable you'll never use?
so why you get data from server, when you would not to use it ?
Yeah. I don't see why you'd be likely to write a select that computes
columns you don't actually want.Tom - your proposal release of stored dataset just before next
statement, not like now on the end of statement?Huh? I think you'd assign the values to the variables and then PQclear
the result right away.
yes - I didn't understand \g mechanism well.
Here is patch - it is not nice at this moment and it is little bit
longer than I expected - but it works
It supports David's syntax
postgres=# select 'Hello', 'World' \gset a,b
postgres=# \echo :'a' :'b'
'Hello' 'World'
postgres=# select 'Hello', 'World';
?column? │ ?column?
──────────┼──────────
Hello │ World
(1 row)
postgres=# \gset a
to few target variables
postgres=# \gset a,
postgres=# \echo :'a'
'Hello'
Regards
Pavel
Show quoted text
regards, tom lane
Attachments:
gset.patchapplication/octet-stream; name=gset.patchDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 8abadb2..2e8309d 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -748,6 +748,29 @@ exec_command(const char *cmd,
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
diff --git a/src/bin/psql/command.h b/src/bin/psql/command.h
index f0bcea0..57cbe23 100644
--- a/src/bin/psql/command.h
+++ b/src/bin/psql/command.h
@@ -16,6 +16,7 @@ typedef enum _backslashResult
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 330d5ce..5248721 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -826,6 +826,103 @@ PrintQueryResults(PGresult *results)
return success;
}
+/*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed sucessfully, false otherwise.
+ */
+static bool
+StoreQueryResult(PGresult *result)
+{
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("to few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY isnot supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+}
/*
* SendQuery: send the query string to the backend
@@ -953,7 +1050,12 @@ SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
- OK = PrintQueryResults(results);
+ {
+ if (pset.gvars)
+ OK = StoreQueryResult(results);
+ else
+ OK = PrintQueryResults(results);
+ }
}
else
{
@@ -1668,3 +1770,53 @@ expand_tilde(char **filename)
return *filename;
}
+
+
+/*
+ * Add name of internal variable to query targer list
+ *
+ */
+TargetList
+tglist_add(TargetList tglist, const char *name)
+{
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+}
+
+/*
+ * Release target list
+ *
+ */
+void
+tglist_free(TargetList tglist)
+{
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+}
diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h
index 8037cbc..f955fec 100644
--- a/src/bin/psql/common.h
+++ b/src/bin/psql/common.h
@@ -21,6 +21,14 @@
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+typedef struct _target_field
+{
+ char *name;
+ struct _target_field *next;
+} TargetListData;
+
+typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
@@ -63,4 +71,7 @@ extern const char *session_username(void);
extern char *expand_tilde(char **filename);
+extern TargetList tglist_add(TargetList tglist, const char *name);
+extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 3ebf7cc..0f34f41 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -162,13 +162,14 @@ slashUsage(unsigned short int pager)
{
FILE *output;
- output = PageOutput(94, pager);
+ output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index 16c65ec..33faff8 100644
--- a/src/bin/psql/mainloop.c
+++ b/src/bin/psql/mainloop.c
@@ -327,6 +327,14 @@ MainLoop(FILE *source)
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
diff --git a/src/bin/psql/psqlscan.h b/src/bin/psql/psqlscan.h
index 0b5ef0c..742de7f 100644
--- a/src/bin/psql/psqlscan.h
+++ b/src/bin/psql/psqlscan.h
@@ -8,6 +8,7 @@
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+#include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
@@ -33,7 +34,8 @@ enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
- OT_NO_EVAL /* no expansion of backticks or variables */
+ OT_NO_EVAL, /* no expansion of backticks or variables */
+ OT_VARLIST /* returns variable's identifier or comma */
};
@@ -59,6 +61,10 @@ extern char *psql_scan_slash_option(PsqlScanState state,
char *quote,
bool semicolon);
+extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
diff --git a/src/bin/psql/psqlscan.l b/src/bin/psql/psqlscan.l
index 1208c8f..15018af 100644
--- a/src/bin/psql/psqlscan.l
+++ b/src/bin/psql/psqlscan.l
@@ -106,13 +106,18 @@ static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
+TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
-#define LEXRES_BACKSLASH 2 /* backslash command start */
-#define LEXRES_OK 3 /* OK completion of backslash argument */
-
+#define LEXRES_COMMA 2
+#define LEXRES_BACKSLASH 3 /* backslash command start */
+#define LEXRES_IDENT 4 /* valid internal variable identifier */
+#define LEXRES_OK 5 /* OK completion of backslash argument */
+#define LEXRES_ERROR 6
+#define LEXRES_NONE 7
int yylex(void);
@@ -167,6 +172,7 @@ static void escape_variable(bool as_ident);
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
@@ -183,6 +189,7 @@ static void escape_variable(bool as_ident);
%x xdolq
%x xui
%x xus
+%x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
@@ -628,6 +635,26 @@ other .
ECHO;
}
+<xvl>{
+
+{identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+"," {
+ return LEXRES_COMMA;
+ }
+
+{horiz_space}+ { }
+
+. |
+\n {
+ return LEXRES_ERROR;
+ }
+
+}
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
@@ -1449,6 +1476,75 @@ psql_scan_slash_command(PsqlScanState state)
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+TargetList
+psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+{
+ PQExpBufferData mybuf;
+ int lexresult = LEXRES_NONE;
+ int lexresult_prev;
+ bool is_lexer_error = false;
+
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ gvars = NULL;
+
+ do
+ {
+ lexresult_prev = lexresult;
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ /* read to end */
+ if (is_lexer_error)
+ continue;
+
+ if (lexresult == LEXRES_ERROR)
+ {
+ is_lexer_error = true;
+ continue;
+ }
+
+ if (lexresult_prev == LEXRES_IDENT && lexresult == LEXRES_IDENT)
+ {
+ is_lexer_error = true;
+ continue;
+ }
+
+ if (!is_lexer_error && (lexresult_prev == LEXRES_COMMA || lexresult_prev == LEXRES_NONE)
+ && lexresult == LEXRES_COMMA)
+ gvars = tglist_add(gvars, NULL);
+
+ } while (lexresult != LEXRES_EOL);
+
+ if (!is_lexer_error && lexresult_prev == LEXRES_COMMA)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = is_lexer_error;
+
+ return gvars;
+}
+
+/*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index c907fa0..e3381b2 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -9,6 +9,7 @@
#define SETTINGS_H
+#include "common.h"
#include "variables.h"
#include "print.h"
@@ -73,6 +74,7 @@ typedef struct _psqlSettings
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index a1bb230..b47a44a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -811,7 +811,7 @@ psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
- "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
+ "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
On Sat, Jul 28, 2012 at 06:11:21PM +0200, Pavel Stehule wrote:
Hello
2012/7/27 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/7/26 David Fetter <david@fetter.org>:
How about
\gset var1,,,var2,var3...I don't like this - you can use fake variable - and ignoring some
variable has no big effect on clientWhy assign to a variable you'll never use?
so why you get data from server, when you would not to use it ?
Yeah. I don't see why you'd be likely to write a select that computes
columns you don't actually want.Tom - your proposal release of stored dataset just before next
statement, not like now on the end of statement?Huh? I think you'd assign the values to the variables and then PQclear
the result right away.yes - I didn't understand \g mechanism well.
Here is patch - it is not nice at this moment and it is little bit
longer than I expected - but it worksIt supports David's syntax
postgres=# select 'Hello', 'World' \gset a,b
postgres=# \echo :'a' :'b'
'Hello' 'World'
postgres=# select 'Hello', 'World';
?column? │ ?column?
──────────┼──────────
Hello │ World
(1 row)postgres=# \gset a
to few target variables
postgres=# \gset a,
postgres=# \echo :'a'
'Hello'Regards
Pavel
Teensy code cleanup (trailing space) and SGML docs added.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
Attachments:
gset_02.difftext/plain; charset=us-asciiDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1489,1495 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
<literal>\r</> to cancel.
</para>
--- 1489,1495 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or <literal>\gset</literal> to send it, or
<literal>\r</> to cancel.
</para>
***************
*** 1623,1628 **** Tue Oct 26 21:40:57 CEST 1999
--- 1623,1640 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores
+ the query's target list a corresponding list of psql
+ variables.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 826,831 **** PrintQueryResults(PGresult *results)
--- 826,928 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed sucessfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("to few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY isnot supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 953,959 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1050,1061 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1668,1670 **** expand_tilde(char **filename)
--- 1770,1822 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query targer list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 63,66 **** extern const char *session_username(void);
--- 71,77 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,123 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6
! #define LEXRES_NONE 7
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 172,178 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 189,195 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 635,660 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1476,1550 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult = LEXRES_NONE;
+ int lexresult_prev;
+ bool is_lexer_error = false;
+
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ gvars = NULL;
+
+ do
+ {
+ lexresult_prev = lexresult;
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ /* read to end */
+ if (is_lexer_error)
+ continue;
+
+ if (lexresult == LEXRES_ERROR)
+ {
+ is_lexer_error = true;
+ continue;
+ }
+
+ if (lexresult_prev == LEXRES_IDENT && lexresult == LEXRES_IDENT)
+ {
+ is_lexer_error = true;
+ continue;
+ }
+
+ if (!is_lexer_error && (lexresult_prev == LEXRES_COMMA || lexresult_prev == LEXRES_NONE)
+ && lexresult == LEXRES_COMMA)
+ gvars = tglist_add(gvars, NULL);
+
+ } while (lexresult != LEXRES_EOL);
+
+ if (!is_lexer_error && lexresult_prev == LEXRES_COMMA)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = is_lexer_error;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 811,817 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 811,817 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
Hello
there is new version of this patch
* cleaned var list parser
* new regress tests
* support FETCH_COUNT > 0
Regards
Pavel Stehule
2012/8/1 David Fetter <david@fetter.org>:
Show quoted text
On Sat, Jul 28, 2012 at 06:11:21PM +0200, Pavel Stehule wrote:
Hello
2012/7/27 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/7/26 David Fetter <david@fetter.org>:
How about
\gset var1,,,var2,var3...I don't like this - you can use fake variable - and ignoring some
variable has no big effect on clientWhy assign to a variable you'll never use?
so why you get data from server, when you would not to use it ?
Yeah. I don't see why you'd be likely to write a select that computes
columns you don't actually want.Tom - your proposal release of stored dataset just before next
statement, not like now on the end of statement?Huh? I think you'd assign the values to the variables and then PQclear
the result right away.yes - I didn't understand \g mechanism well.
Here is patch - it is not nice at this moment and it is little bit
longer than I expected - but it worksIt supports David's syntax
postgres=# select 'Hello', 'World' \gset a,b
postgres=# \echo :'a' :'b'
'Hello' 'World'
postgres=# select 'Hello', 'World';
?column? │ ?column?
──────────┼──────────
Hello │ World
(1 row)postgres=# \gset a
to few target variables
postgres=# \gset a,
postgres=# \echo :'a'
'Hello'Regards
Pavel
Teensy code cleanup (trailing space) and SGML docs added.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.icsRemember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
Attachments:
gset_03.diffapplication/octet-stream; name=gset_03.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1490,1496 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
<literal>\r</> to cancel.
</para>
--- 1490,1496 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or <literal>\gset</literal> to send it, or
<literal>\r</> to cancel.
</para>
***************
*** 1624,1629 **** Tue Oct 26 21:40:57 CEST 1999
--- 1624,1641 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores
+ the query's target list a corresponding list of psql
+ variables.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars && !error)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 826,831 **** PrintQueryResults(PGresult *results)
--- 826,928 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed sucessfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("to few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY isnot supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 953,959 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1050,1061 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1077,1082 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1179,1186 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1143,1148 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1247,1255 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1192,1198 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1299,1321 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1218,1223 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1341,1348 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1668,1670 **** expand_tilde(char **filename)
--- 1793,1845 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query targer list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 63,66 **** extern const char *session_username(void);
--- 71,77 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,122 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2 /* comma as identifiers list separator */
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6 /* unexpected char in identifiers list */
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 171,177 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 188,194 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 634,659 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1475,1571 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ typedef enum
+ {
+ VARLIST_PARSER_INITIAL,
+ VARLIST_PARSER_EXPECTED_COMMA,
+ VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT,
+ VARLIST_PARSER_ERROR
+ } VarListParserState;
+
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+ VarListParserState vlpState; /* parser state */
+
+ gvars = NULL;
+ vlpState = VARLIST_PARSER_INITIAL;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ while (lexresult != LEXRES_EOL)
+ {
+ /* read to EOL, skip processing when some error occurs */
+ if (vlpState != VARLIST_PARSER_ERROR)
+ {
+ switch (lexresult)
+ {
+ case LEXRES_ERROR:
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ case LEXRES_COMMA:
+ /* insert /dev/null targets */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT;
+ break;
+
+ case LEXRES_IDENT:
+ /* don't allow var after var without comma */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA;
+ else
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ default:
+ /* can't get here */
+ fprintf(stderr, "invalid yylex result\n");
+ exit(1);
+ }
+ }
+
+ /* read next token */
+ BEGIN(xvl);
+ lexresult = yylex();
+ }
+
+ /* append /dev/null target when last token was comma */
+ if (vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = vlpState == VARLIST_PARSER_ERROR;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 811,817 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 811,817 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql.out
***************
*** 0 ****
--- 1,38 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ -- should fail
+ \gset ,,
+ \gset ,
+ to few target variables
+ \gset ,,,
+ too many target variables
+ -- should be ok
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 133,135 **** test: largeobject
--- 133,136 ----
test: with
test: xml
test: stats
+ test: psql
*** /dev/null
--- b/src/test/regress/sql/psql.sql
***************
*** 0 ****
--- 1,37 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ -- should fail
+ \gset ,,
+ \gset ,
+ \gset ,,,
+
+ -- should be ok
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
On Fri, Aug 10, 2012 at 3:21 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:
there is new version of this patch
* cleaned var list parser
* new regress tests
* support FETCH_COUNT > 0
Here are my review comments.
Submission
==========
The patch is formatted in context diff style, and it could be applied
cleanly against latest master. This patch include document and tests,
but IMO they need some enhancement.
Usability
=========
This patch provides new psql command \gset which sends content of query
buffer to server, and stores result of the query into psql variables.
The name "\gset" is mixture of \g, which sends result to file or pipe,
and \set, which sets variable to some value, so it would sound natural
to psql users.
Freature test
=============
Compile completed without warning. Regression tests for \gset passed,
but I have some comments on them.
- Other regression tests have comment "-- ERROR" just after queries
which should fail. It would be nice to follow this manner.
- Typo "to few" in expected file and source file.
- How about adding testing "\gset" (no variable list) to "should fail"?
- Is it intentional that \gset can set special variables such as
AUTOCOMMIT and HOST? I don't see any downside for this behavior,
because \set also can do that, but it is not documented nor tested at all.
Document
========
- Adding some description of \gset command, especially about limitation
of variable list, seems necessary.
- In addition to the meta-command section, "Advanced features" section
mentions how to set psql's variables, so we would need some mention
there too.
- The term "target list" might not be familiar to users, since it
appears in only sections mentioning PG internal relatively. I think
that the feature described in the section "Retrieving Query Results" in
ECPG document is similar to this feature.
http://www.postgresql.org/docs/devel/static/ecpg-variables.html
Coding
======
The code follows our coding conventions. Here are comments for coding.
- Some typo found in comments, please see attached patch.
- There is a code path which doesn't print error message even if libpq
reported error (PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
PGRES_FATAL_ERROR) in StoreQueryResult. Is this intentional? FYI, ecpg
prints "bad response" message for those errors.
Although I'll look the code more closely later, but anyway I marked the
patch "Waiting on Author" for comments above.
Regards,
--
Shigeru HANADA
Attachments:
fix_typo.patchtext/plain; charset=Shift_JIS; name=fix_typo.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 0e9b408..a76b84d 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -832,7 +832,7 @@ PrintQueryResults(PGresult *results)
*
* Note: Utility function for use by SendQuery() only.
*
- * Returns true if the query executed sucessfully, false otherwise.
+ * Returns true if the query executed successfully, false otherwise.
*/
static bool
StoreQueryResult(PGresult *result)
@@ -865,7 +865,7 @@ StoreQueryResult(PGresult *result)
{
if (!iter)
{
- psql_error("to few target variables\n");
+ psql_error("too few target variables\n");
success = false;
break;
}
@@ -902,7 +902,7 @@ StoreQueryResult(PGresult *result)
case PGRES_COPY_OUT:
case PGRES_COPY_IN:
- psql_error("COPY isnot supported by \\gset command\n");
+ psql_error("COPY is not supported by \\gset command\n");
success = false;
break;
@@ -1797,7 +1797,7 @@ expand_tilde(char **filename)
/*
- * Add name of internal variable to query targer list
+ * Add name of internal variable to query target list
*
*/
TargetList
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 90ab9bd..54fa490 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -15,7 +15,7 @@ select 10, 20, 'Hello World'
-- should fail
\gset ,,
\gset ,
-to few target variables
+too few target variables
\gset ,,,
too many target variables
-- should be ok
Hello
2012/9/19 Shigeru HANADA <shigeru.hanada@gmail.com>:
On Fri, Aug 10, 2012 at 3:21 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:there is new version of this patch
* cleaned var list parser
* new regress tests
* support FETCH_COUNT > 0Here are my review comments.
Submission
==========
The patch is formatted in context diff style, and it could be applied
cleanly against latest master. This patch include document and tests,
but IMO they need some enhancement.Usability
=========
This patch provides new psql command \gset which sends content of query
buffer to server, and stores result of the query into psql variables.
The name "\gset" is mixture of \g, which sends result to file or pipe,
and \set, which sets variable to some value, so it would sound natural
to psql users.Freature test
=============
Compile completed without warning. Regression tests for \gset passed,
but I have some comments on them.- Other regression tests have comment "-- ERROR" just after queries
which should fail. It would be nice to follow this manner.
- Typo "to few" in expected file and source file.
- How about adding testing "\gset" (no variable list) to "should fail"?
- Is it intentional that \gset can set special variables such as
AUTOCOMMIT and HOST? I don't see any downside for this behavior,
because \set also can do that, but it is not documented nor tested at all.
I use a same "SetVariable" function, so a behave should be same
Document
========
- Adding some description of \gset command, especially about limitation
of variable list, seems necessary.
- In addition to the meta-command section, "Advanced features" section
mentions how to set psql's variables, so we would need some mention
there too.
- The term "target list" might not be familiar to users, since it
appears in only sections mentioning PG internal relatively. I think
that the feature described in the section "Retrieving Query Results" in
ECPG document is similar to this feature.
http://www.postgresql.org/docs/devel/static/ecpg-variables.html
I invite any proposals about enhancing documentation. Personally I am
a PostgreSQL developer, so I don't known any different term other than
"target list" - but any user friendly description is welcome.
Coding
======
The code follows our coding conventions. Here are comments for coding.- Some typo found in comments, please see attached patch.
- There is a code path which doesn't print error message even if libpq
reported error (PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
PGRES_FATAL_ERROR) in StoreQueryResult. Is this intentional? FYI, ecpg
prints "bad response" message for those errors.
yes - it is question. I use same pattern like PrintQueryResult, but
"bad response" message should be used.
I am sending updated patch
Show quoted text
Although I'll look the code more closely later, but anyway I marked the
patch "Waiting on Author" for comments above.Regards,
--
Shigeru HANADA
Attachments:
gset_04.diffapplication/octet-stream; name=gset_04.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1489 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
<literal>\r</> to cancel.
</para>
--- 1483,1489 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or <literal>\gset</literal> to send it, or
<literal>\r</> to cancel.
</para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1634 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores
+ the query's target list a corresponding list of psql
+ variables.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars && !error)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 827,832 **** PrintQueryResults(PGresult *results)
--- 827,930 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 954,960 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1052,1063 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1078,1083 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1181,1188 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1144,1149 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1249,1257 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1193,1199 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1301,1323 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1219,1224 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1343,1350 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1669,1671 **** expand_tilde(char **filename)
--- 1795,1847 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 63,66 **** extern const char *session_username(void);
--- 71,77 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,122 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2 /* comma as identifiers list separator */
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6 /* unexpected char in identifiers list */
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 171,177 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 188,194 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 634,659 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1475,1571 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ typedef enum
+ {
+ VARLIST_PARSER_INITIAL,
+ VARLIST_PARSER_EXPECTED_COMMA,
+ VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT,
+ VARLIST_PARSER_ERROR
+ } VarListParserState;
+
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+ VarListParserState vlpState; /* parser state */
+
+ gvars = NULL;
+ vlpState = VARLIST_PARSER_INITIAL;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ while (lexresult != LEXRES_EOL)
+ {
+ /* read to EOL, skip processing when some error occurs */
+ if (vlpState != VARLIST_PARSER_ERROR)
+ {
+ switch (lexresult)
+ {
+ case LEXRES_ERROR:
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ case LEXRES_COMMA:
+ /* insert /dev/null targets */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT;
+ break;
+
+ case LEXRES_IDENT:
+ /* don't allow var after var without comma */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA;
+ else
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ default:
+ /* can't get here */
+ fprintf(stderr, "invalid yylex result\n");
+ exit(1);
+ }
+ }
+
+ /* read next token */
+ BEGIN(xvl);
+ lexresult = yylex();
+ }
+
+ /* append /dev/null target when last token was comma */
+ if (vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = vlpState == VARLIST_PARSER_ERROR;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql.out
***************
*** 0 ****
--- 1,39 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: syntax error
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 133,135 **** test: largeobject
--- 133,136 ----
test: with
test: xml
test: stats
+ test: psql
*** /dev/null
--- b/src/test/regress/sql/psql.sql
***************
*** 0 ****
--- 1,36 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
Hi Pavel,
(2012/09/21 2:01), Pavel Stehule wrote:
- Is it intentional that \gset can set special variables such as
AUTOCOMMIT and HOST? I don't see any downside for this behavior,
because \set also can do that, but it is not documented nor tested at all.I use a same "SetVariable" function, so a behave should be same
It seems reasonable.
Document
========
- Adding some description of \gset command, especially about limitation
of variable list, seems necessary.
- In addition to the meta-command section, "Advanced features" section
mentions how to set psql's variables, so we would need some mention
there too.
- The term "target list" might not be familiar to users, since it
appears in only sections mentioning PG internal relatively. I think
that the feature described in the section "Retrieving Query Results" in
ECPG document is similar to this feature.
http://www.postgresql.org/docs/devel/static/ecpg-variables.htmlI invite any proposals about enhancing documentation. Personally I am
a PostgreSQL developer, so I don't known any different term other than
"target list" - but any user friendly description is welcome.
How about to say "stores the query's result output into variable"?
Please see attached file for my proposal. I also mentioned about 1-row
limit and omit of variable.
Coding
======
The code follows our coding conventions. Here are comments for coding.- Some typo found in comments, please see attached patch.
- There is a code path which doesn't print error message even if libpq
reported error (PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
PGRES_FATAL_ERROR) in StoreQueryResult. Is this intentional? FYI, ecpg
prints "bad response" message for those errors.yes - it is question. I use same pattern like PrintQueryResult, but
"bad response" message should be used.I am sending updated patch
It seems ok.
BTW, as far as I see, no psql backslash command including \setenv (it
was added in 9.2) has regression test in core (I mean src/test/regress).
Is there any convention about this issue? If psql backslash commands
(or any psql feature else) don't need regression test, we can remove
psql.(sql|out).
# Of course we need to test new feature by hand.
Anyway, IMO the name psql impresses larger area than the patch
implements. How about to rename psql to psql_cmd or backslash_cmd than
psql as regression test name?
--
Shigeru HANADA
Attachments:
gset_05.difftext/x-patch; name=gset_05.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3693a5a..c4ac674 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1483,8 +1483,8 @@ testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
- query buffer; type semicolon, <literal>\g</> or <literal>\gset</literal> to send it, or
- <literal>\r</> to cancel.
+ query buffer; type semicolon, <literal>\g</> or
+ <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
@@ -1621,9 +1621,19 @@ Tue Oct 26 21:40:57 CEST 1999
<listitem>
<para>
- Sends the current query input buffer to the server and stores
- the query's target list a corresponding list of psql
- variables.
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+<programlisting>
+foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+foo=> \echo :var1 :var3
+hello world!
+</programlisting>
</para>
</listitem>
</varlistentry>
Hello
2012/9/21 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
(2012/09/21 2:01), Pavel Stehule wrote:
- Is it intentional that \gset can set special variables such as
AUTOCOMMIT and HOST? I don't see any downside for this behavior,
because \set also can do that, but it is not documented nor tested at all.I use a same "SetVariable" function, so a behave should be same
It seems reasonable.
Document
========
- Adding some description of \gset command, especially about limitation
of variable list, seems necessary.
- In addition to the meta-command section, "Advanced features" section
mentions how to set psql's variables, so we would need some mention
there too.
- The term "target list" might not be familiar to users, since it
appears in only sections mentioning PG internal relatively. I think
that the feature described in the section "Retrieving Query Results" in
ECPG document is similar to this feature.
http://www.postgresql.org/docs/devel/static/ecpg-variables.htmlI invite any proposals about enhancing documentation. Personally I am
a PostgreSQL developer, so I don't known any different term other than
"target list" - but any user friendly description is welcome.How about to say "stores the query's result output into variable"?
Please see attached file for my proposal. I also mentioned about 1-row
limit and omit of variable.
should be
Coding
======
The code follows our coding conventions. Here are comments for coding.- Some typo found in comments, please see attached patch.
- There is a code path which doesn't print error message even if libpq
reported error (PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
PGRES_FATAL_ERROR) in StoreQueryResult. Is this intentional? FYI, ecpg
prints "bad response" message for those errors.yes - it is question. I use same pattern like PrintQueryResult, but
"bad response" message should be used.I am sending updated patch
It seems ok.
BTW, as far as I see, no psql backslash command including \setenv (it
was added in 9.2) has regression test in core (I mean src/test/regress).
Is there any convention about this issue? If psql backslash commands
(or any psql feature else) don't need regression test, we can remove
psql.(sql|out).
# Of course we need to test new feature by hand.
It is question for Tom or David - only server side functionalities has
regress tests. But result of some backslash command is verified in
other regress tests. I would to see some regression tests for this
functionality.
Anyway, IMO the name psql impresses larger area than the patch
implements. How about to rename psql to psql_cmd or backslash_cmd than
psql as regression test name?
I have no idea - psql_cmd is good name
Regards
Pavel
Show quoted text
--
Shigeru HANADA
Hello
here is updated version of gset patch.
* merge Shigeru's doc patch
* rename psql regression test from "psql" to "psql_cmd"
Regards
Pavel Stehule
2012/9/27 Pavel Stehule <pavel.stehule@gmail.com>:
Show quoted text
Hello
2012/9/21 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
(2012/09/21 2:01), Pavel Stehule wrote:
- Is it intentional that \gset can set special variables such as
AUTOCOMMIT and HOST? I don't see any downside for this behavior,
because \set also can do that, but it is not documented nor tested at all.I use a same "SetVariable" function, so a behave should be same
It seems reasonable.
Document
========
- Adding some description of \gset command, especially about limitation
of variable list, seems necessary.
- In addition to the meta-command section, "Advanced features" section
mentions how to set psql's variables, so we would need some mention
there too.
- The term "target list" might not be familiar to users, since it
appears in only sections mentioning PG internal relatively. I think
that the feature described in the section "Retrieving Query Results" in
ECPG document is similar to this feature.
http://www.postgresql.org/docs/devel/static/ecpg-variables.htmlI invite any proposals about enhancing documentation. Personally I am
a PostgreSQL developer, so I don't known any different term other than
"target list" - but any user friendly description is welcome.How about to say "stores the query's result output into variable"?
Please see attached file for my proposal. I also mentioned about 1-row
limit and omit of variable.should be
Coding
======
The code follows our coding conventions. Here are comments for coding.- Some typo found in comments, please see attached patch.
- There is a code path which doesn't print error message even if libpq
reported error (PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
PGRES_FATAL_ERROR) in StoreQueryResult. Is this intentional? FYI, ecpg
prints "bad response" message for those errors.yes - it is question. I use same pattern like PrintQueryResult, but
"bad response" message should be used.I am sending updated patch
It seems ok.
BTW, as far as I see, no psql backslash command including \setenv (it
was added in 9.2) has regression test in core (I mean src/test/regress).
Is there any convention about this issue? If psql backslash commands
(or any psql feature else) don't need regression test, we can remove
psql.(sql|out).
# Of course we need to test new feature by hand.It is question for Tom or David - only server side functionalities has
regress tests. But result of some backslash command is verified in
other regress tests. I would to see some regression tests for this
functionality.Anyway, IMO the name psql impresses larger area than the patch
implements. How about to rename psql to psql_cmd or backslash_cmd than
psql as regression test name?I have no idea - psql_cmd is good name
Regards
Pavel
--
Shigeru HANADA
Attachments:
gset_06.diffapplication/octet-stream; name=gset_06.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars && !error)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1177 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1133,1138 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1238,1246 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1290,1312 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1208,1213 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1332,1339 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1784,1836 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,122 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2 /* comma as identifiers list separator */
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6 /* unexpected char in identifiers list */
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 171,177 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 188,194 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 634,659 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1475,1571 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ typedef enum
+ {
+ VARLIST_PARSER_INITIAL,
+ VARLIST_PARSER_EXPECTED_COMMA,
+ VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT,
+ VARLIST_PARSER_ERROR
+ } VarListParserState;
+
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+ VarListParserState vlpState; /* parser state */
+
+ gvars = NULL;
+ vlpState = VARLIST_PARSER_INITIAL;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ while (lexresult != LEXRES_EOL)
+ {
+ /* read to EOL, skip processing when some error occurs */
+ if (vlpState != VARLIST_PARSER_ERROR)
+ {
+ switch (lexresult)
+ {
+ case LEXRES_ERROR:
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ case LEXRES_COMMA:
+ /* insert /dev/null targets */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT;
+ break;
+
+ case LEXRES_IDENT:
+ /* don't allow var after var without comma */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA;
+ else
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ default:
+ /* can't get here */
+ fprintf(stderr, "invalid yylex result\n");
+ exit(1);
+ }
+ }
+
+ /* read next token */
+ BEGIN(xvl);
+ lexresult = yylex();
+ }
+
+ /* append /dev/null target when last token was comma */
+ if (vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = vlpState == VARLIST_PARSER_ERROR;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,39 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: syntax error
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,36 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
Hi Pavel,
On Sat, Oct 13, 2012 at 12:58 AM, Pavel Stehule
<pavel.stehule@gmail.com> wrote:
* merge Shigeru's doc patch
* rename psql regression test from "psql" to "psql_cmd"
Those changes seem good.
Besides, I found an error message which doesn't end with '¥n' in
common.c. In general, messages passed to psql_error end with '¥n',
unless additional information follows. Please see attached patch for
additional change.
After you determine whether it's ok or unnecessary, I'll mark this patch
as "Ready for committer".
Regards,
--
Shigeru HANADA
Attachments:
gset_07.difftext/plain; charset=UTF-8; name=gset_07.diff; x-mac-creator=0; x-mac-type=0Download
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars && !error)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1177 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1133,1138 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1238,1246 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1290,1312 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1208,1213 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1332,1339 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1784,1836 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,122 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2 /* comma as identifiers list separator */
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6 /* unexpected char in identifiers list */
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 171,177 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 188,194 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 634,659 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1475,1571 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ typedef enum
+ {
+ VARLIST_PARSER_INITIAL,
+ VARLIST_PARSER_EXPECTED_COMMA,
+ VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT,
+ VARLIST_PARSER_ERROR
+ } VarListParserState;
+
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+ VarListParserState vlpState; /* parser state */
+
+ gvars = NULL;
+ vlpState = VARLIST_PARSER_INITIAL;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ while (lexresult != LEXRES_EOL)
+ {
+ /* read to EOL, skip processing when some error occurs */
+ if (vlpState != VARLIST_PARSER_ERROR)
+ {
+ switch (lexresult)
+ {
+ case LEXRES_ERROR:
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ case LEXRES_COMMA:
+ /* insert /dev/null targets */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT;
+ break;
+
+ case LEXRES_IDENT:
+ /* don't allow var after var without comma */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA;
+ else
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ default:
+ /* can't get here */
+ fprintf(stderr, "invalid yylex result\n");
+ exit(1);
+ }
+ }
+
+ /* read next token */
+ BEGIN(xvl);
+ lexresult = yylex();
+ }
+
+ /* append /dev/null target when last token was comma */
+ if (vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = vlpState == VARLIST_PARSER_ERROR;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,39 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: syntax error
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,36 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
2012/10/13 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
On Sat, Oct 13, 2012 at 12:58 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:* merge Shigeru's doc patch
* rename psql regression test from "psql" to "psql_cmd"Those changes seem good.
Besides, I found an error message which doesn't end with '¥n' in common.c.
In general, messages passed to psql_error end with '¥n', unless additional
information follows. Please see attached patch for additional change.After you determine whether it's ok or unnecessary, I'll mark this patch as
"Ready for committer".
it is ok, thank you
Pavel
Show quoted text
Regards,
--
Shigeru HANADA
On Sat, October 13, 2012 19:26, Pavel Stehule wrote:
2012/10/13 Shigeru HANADA <shigeru.hanada@gmail.com>:
After you determine whether it's ok or unnecessary, I'll mark this patch as
"Ready for committer".
I found this behaviour which I think must count as a bug.
\gset doesn't allow more \\-separated lines behind it:
Only the last of these commands is problematic, and giving the syntax error
$ psql
psql (9.3devel-psql_var-20121012_2345-8b728e5c6e0ce6b6d6f54b92b390f14aa1aca6db)
Type "help" for help.
testdb=# select 1,2 \gset x,y
testdb=# \echo :x
1
testdb=# \echo :y
2
testdb=# \echo :x \\ \echo :y
1
2
testdb=# select 1,2 \gset x,y \\ \echo :x
\gset: syntax error
testdb=#
It'd be nice if it could be made to work
Thanks
Erik Rijkers
Hello
2012/10/14 Erik Rijkers <er@xs4all.nl>:
On Sat, October 13, 2012 19:26, Pavel Stehule wrote:
2012/10/13 Shigeru HANADA <shigeru.hanada@gmail.com>:
After you determine whether it's ok or unnecessary, I'll mark this patch as
"Ready for committer".I found this behaviour which I think must count as a bug.
\gset doesn't allow more \\-separated lines behind it:Only the last of these commands is problematic, and giving the syntax error
$ psql
psql (9.3devel-psql_var-20121012_2345-8b728e5c6e0ce6b6d6f54b92b390f14aa1aca6db)
Type "help" for help.testdb=# select 1,2 \gset x,y
testdb=# \echo :x
1
testdb=# \echo :y
2
testdb=# \echo :x \\ \echo :y
1
2
testdb=# select 1,2 \gset x,y \\ \echo :x
\gset: syntax error
testdb=#It'd be nice if it could be made to work
done
Regards
Pavel
Show quoted text
Thanks
Erik Rijkers
Attachments:
gset_08.diffapplication/octet-stream; name=gset_08.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,776 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool error;
+
+ pset.gvars = psql_scan_slash_vars(scan_state, &error);
+
+ if (!pset.gvars && !error)
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ }
+ else if (error)
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ status = PSQL_CMD_NOSEND;
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ }
+ else
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1177 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1133,1138 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1238,1246 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1290,1312 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1208,1213 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1332,1339 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1784,1836 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,70 ----
char *quote,
bool semicolon);
+ extern TargetList psql_scan_slash_vars(PsqlScanState state,
+ bool *error);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 106,118 **** static char *option_quote;
static int unquoted_option_chars;
static int backtick_start_offset;
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_BACKSLASH 2 /* backslash command start */
! #define LEXRES_OK 3 /* OK completion of backslash argument */
!
int yylex(void);
--- 106,122 ----
static int unquoted_option_chars;
static int backtick_start_offset;
+ TargetList gvars;
+
/* Return values from yylex() */
#define LEXRES_EOL 0 /* end of input */
#define LEXRES_SEMI 1 /* command-terminating semicolon found */
! #define LEXRES_COMMA 2 /* comma as identifiers list separator */
! #define LEXRES_BACKSLASH 3 /* backslash command start */
! #define LEXRES_IDENT 4 /* valid internal variable identifier */
! #define LEXRES_OK 5 /* OK completion of backslash argument */
! #define LEXRES_ERROR 6 /* unexpected char in identifiers list */
int yylex(void);
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 171,177 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of variables
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 188,194 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 634,663 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ gvars = tglist_add(gvars, yytext);
+ return LEXRES_IDENT;
+ }
+
+ "," {
+ return LEXRES_COMMA;
+ }
+
+ "\\\\" {
+ return LEXRES_EOL;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ return LEXRES_ERROR;
+ }
+
+ }
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1449,1454 **** psql_scan_slash_command(PsqlScanState state)
--- 1479,1575 ----
}
/*
+ * returns identifier from identifier list
+ *
+ * when comma_expected is true, when we require comma before identifier
+ * error is true, when unexpected char was identified or missing
+ * comma.
+ *
+ */
+ typedef enum
+ {
+ VARLIST_PARSER_INITIAL,
+ VARLIST_PARSER_EXPECTED_COMMA,
+ VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT,
+ VARLIST_PARSER_ERROR
+ } VarListParserState;
+
+ TargetList
+ psql_scan_slash_vars(PsqlScanState state,
+ bool *error)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+ VarListParserState vlpState; /* parser state */
+
+ gvars = NULL;
+ vlpState = VARLIST_PARSER_INITIAL;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ while (lexresult != LEXRES_EOL)
+ {
+ /* read to EOL, skip processing when some error occurs */
+ if (vlpState != VARLIST_PARSER_ERROR)
+ {
+ switch (lexresult)
+ {
+ case LEXRES_ERROR:
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ case LEXRES_COMMA:
+ /* insert /dev/null targets */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT;
+ break;
+
+ case LEXRES_IDENT:
+ /* don't allow var after var without comma */
+ if (vlpState == VARLIST_PARSER_INITIAL ||
+ vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ vlpState = VARLIST_PARSER_EXPECTED_COMMA;
+ else
+ vlpState = VARLIST_PARSER_ERROR;
+ break;
+
+ default:
+ /* can't get here */
+ fprintf(stderr, "invalid yylex result\n");
+ exit(1);
+ }
+ }
+
+ /* read next token */
+ BEGIN(xvl);
+ lexresult = yylex();
+ }
+
+ /* append /dev/null target when last token was comma */
+ if (vlpState == VARLIST_PARSER_EXPECTED_COMMA_OR_IDENT)
+ gvars = tglist_add(gvars, NULL);
+
+ *error = vlpState == VARLIST_PARSER_ERROR;
+
+ return gvars;
+ }
+
+ /*
* Parse off the next argument for a backslash command, and return it as a
* malloc'd string. If there are no more arguments, returns NULL.
*
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,43 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: syntax error
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ (1 row)
+
+ 1
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,38 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_08.diff ]
In the course of adding a new backslash command, this patch manages to
touch:
* the main loop's concept of what types of backslash commands exist
(PSQL_CMD_NOSEND ... what's the point of that, rather than making
this work the same as \g?)
* SendQuery's concept of how to process command results (again, why
isn't this more like \g?)
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)
* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)
* the core psql settings construct (to store something that is in
no way a persistent setting)
Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.
regards, tom lane
Hello
fast reply
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_08.diff ]
In the course of adding a new backslash command, this patch manages to
touch:* the main loop's concept of what types of backslash commands exist
(PSQL_CMD_NOSEND ... what's the point of that, rather than making
this work the same as \g?)
* SendQuery's concept of how to process command results (again, why
isn't this more like \g?)
If I remember, I had to do because I had a problem with shell, but I
have to diagnose it again
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)
There was two possibilities, but hardly non using cursor is better way
* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)
??
* the core psql settings construct (to store something that is in
no way a persistent setting)
??
Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.
- all these bugs are based on lexer construct. A little modification
of lexer is possible
Regards
Pavel
Show quoted text
regards, tom lane
Hello
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_08.diff ]
In the course of adding a new backslash command, this patch manages to
touch:* the main loop's concept of what types of backslash commands exist
(PSQL_CMD_NOSEND ... what's the point of that, rather than making
this work the same as \g?)
This is necessary, because there is a new possible state - "query is
complete, but command is wrong" - so I cannot use \g implementation,
because there is no possible error in \g or ';'
* SendQuery's concept of how to process command results (again, why
isn't this more like \g?)
it is similar - difference is only in work with result - \gset uses
StoreQueryResult instead PrintQueryResults, but other is same
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)
It is my mistake - simply and correct way is not using cursor in this use case
* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)
I had to modify lexer - current lexer supports symbols separated by
space, but not symbols separated by comma. We don't would to use
variable evaluation in target variable list.
* the core psql settings construct (to store something that is in
no way a persistent setting)
sorry, I don't understand to this issue
Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.
I had too strong in checking and raising errors. Is relative simple to
modify patch to enable more backslash statements on same line
I'll send updated patch early
Regards
Pavel
Show quoted text
regards, tom lane
2012/10/15 Pavel Stehule <pavel.stehule@gmail.com>:
Hello
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_08.diff ]
In the course of adding a new backslash command, this patch manages to
touch:* the main loop's concept of what types of backslash commands exist
(PSQL_CMD_NOSEND ... what's the point of that, rather than making
this work the same as \g?)This is necessary, because there is a new possible state - "query is
complete, but command is wrong" - so I cannot use \g implementation,
because there is no possible error in \g or ';'* SendQuery's concept of how to process command results (again, why
isn't this more like \g?)it is similar - difference is only in work with result - \gset uses
StoreQueryResult instead PrintQueryResults, but other is same* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)It is my mistake - simply and correct way is not using cursor in this use case
and it is question if cursor support is bad decision, because cursors
can help with large queries, that can returns thousands rows - and we
can raise error early, before we fall on "out of memory"
regards
Pavel
Show quoted text
* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)I had to modify lexer - current lexer supports symbols separated by
space, but not symbols separated by comma. We don't would to use
variable evaluation in target variable list.* the core psql settings construct (to store something that is in
no way a persistent setting)sorry, I don't understand to this issue
Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.I had too strong in checking and raising errors. Is relative simple to
modify patch to enable more backslash statements on same lineI'll send updated patch early
Regards
Pavel
regards, tom lane
Hi Pavel,
First of all, I'm sorry that my previous review was rough. I looked
your patch and existing code closely again.
On 2012/10/15, at 12:57, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)There was two possibilities, but hardly non using cursor is better way
+1 for supporting the case when FETCH_COUNT > 0, because user might set
so mainly for other queries, and they would want to avoid changing
FETCH_COUNT setting during every query followed by \gset.
* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)??
I think that Tom is talking about psql_scan_slash_vars(). It seems too
specific to \gset command. How about to improve
psql_scan_slash_options() so that it can handle comma-separated variable
list? Although you might have tried it before.
# Unused OT_VARLIST macro gave me the idea.
* the core psql settings construct (to store something that is in
no way a persistent setting)??
I thought that having \gset arguments in pset is reasonable, since \g
uses pest.gfname to hold its one-shot setting. Or, we should refactor
\g to fit with \gset? I might be missing Tom's point...
Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.- all these bugs are based on lexer construct. A little modification
of lexer is possible
IMHO those issues come from the design rather than the implementation of
lexer. AFAIK we don't have consensus about the case that both of \g and
\gset are used for a query like this:
postgres=# SELECT 1 \gset var \\ \g foo.txt
This seems regal. Should we store "1" into var and write the result
into foo.txt? Or, only either of them? It's just an idea and it
requires new special character, but how about use \g command for file,
pipe, and variable? In the case we choose '&' for variable prefix:
postgres=# SELECT 'hello', 'wonderful', 'world!' \g &var1,,var2
Anyway, we've had no psql's meta command which processes query result
other than \g. So, we might have more considerable issues about design.
BTW, what the word "comma_expected" means? It's in the comment above
psql_scan_slash_vars(). It might be a remaining of old implementation.
Regards,
--
Shigeru HANADA
shigeru.hanada@gmail.com
2012/10/15 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
First of all, I'm sorry that my previous review was rough. I looked
your patch and existing code closely again.On 2012/10/15, at 12:57, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)There was two possibilities, but hardly non using cursor is better way
+1 for supporting the case when FETCH_COUNT > 0, because user might set
so mainly for other queries, and they would want to avoid changing
FETCH_COUNT setting during every query followed by \gset.* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)??
I think that Tom is talking about psql_scan_slash_vars(). It seems too
specific to \gset command. How about to improve
psql_scan_slash_options() so that it can handle comma-separated variable
list? Although you might have tried it before.
# Unused OT_VARLIST macro gave me the idea.
yes, it is possible - I'll look on it at evening
* the core psql settings construct (to store something that is in
no way a persistent setting)??
I thought that having \gset arguments in pset is reasonable, since \g
uses pest.gfname to hold its one-shot setting. Or, we should refactor
\g to fit with \gset? I might be missing Tom's point...Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.- all these bugs are based on lexer construct. A little modification
of lexer is possibleIMHO those issues come from the design rather than the implementation of
lexer. AFAIK we don't have consensus about the case that both of \g and
\gset are used for a query like this:postgres=# SELECT 1 \gset var \\ \g foo.txt
This seems regal. Should we store "1" into var and write the result
into foo.txt? Or, only either of them? It's just an idea and it
requires new special character, but how about use \g command for file,
pipe, and variable? In the case we choose '&' for variable prefix:postgres=# SELECT 'hello', 'wonderful', 'world!' \g &var1,,var2
Anyway, we've had no psql's meta command which processes query result
other than \g. So, we might have more considerable issues about design.
a current design is rigid - a small implementation can stop parsing
target list, when other backslash statement is detected
BTW, what the word "comma_expected" means? It's in the comment above
psql_scan_slash_vars(). It might be a remaining of old implementation.
This identifier is mistaken - etc this comment is wrong and related to
old implementation - sorry. A first design was replaced by state
machine described by VarListParserState
Show quoted text
Regards,
--
Shigeru HANADA
shigeru.hanada@gmail.com
2012/10/15 Pavel Stehule <pavel.stehule@gmail.com>:
2012/10/15 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
First of all, I'm sorry that my previous review was rough. I looked
your patch and existing code closely again.On 2012/10/15, at 12:57, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)There was two possibilities, but hardly non using cursor is better way
+1 for supporting the case when FETCH_COUNT > 0, because user might set
so mainly for other queries, and they would want to avoid changing
FETCH_COUNT setting during every query followed by \gset.* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)??
I think that Tom is talking about psql_scan_slash_vars(). It seems too
specific to \gset command. How about to improve
psql_scan_slash_options() so that it can handle comma-separated variable
list? Although you might have tried it before.
# Unused OT_VARLIST macro gave me the idea.yes, it is possible - I'll look on it at evening
a reuse of psql_scan_slash_options is not good idea - a interface of
this function is out of my purposes - and I cannot to signalise error
from this procedure. But I can minimize psql_scan_slash_var and I can
move lot of code out of lexer file.
Show quoted text
* the core psql settings construct (to store something that is in
no way a persistent setting)??
I thought that having \gset arguments in pset is reasonable, since \g
uses pest.gfname to hold its one-shot setting. Or, we should refactor
\g to fit with \gset? I might be missing Tom's point...Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.- all these bugs are based on lexer construct. A little modification
of lexer is possibleIMHO those issues come from the design rather than the implementation of
lexer. AFAIK we don't have consensus about the case that both of \g and
\gset are used for a query like this:postgres=# SELECT 1 \gset var \\ \g foo.txt
This seems regal. Should we store "1" into var and write the result
into foo.txt? Or, only either of them? It's just an idea and it
requires new special character, but how about use \g command for file,
pipe, and variable? In the case we choose '&' for variable prefix:postgres=# SELECT 'hello', 'wonderful', 'world!' \g &var1,,var2
Anyway, we've had no psql's meta command which processes query result
other than \g. So, we might have more considerable issues about design.a current design is rigid - a small implementation can stop parsing
target list, when other backslash statement is detectedBTW, what the word "comma_expected" means? It's in the comment above
psql_scan_slash_vars(). It might be a remaining of old implementation.This identifier is mistaken - etc this comment is wrong and related to
old implementation - sorry. A first design was replaced by state
machine described by VarListParserStateRegards,
--
Shigeru HANADA
shigeru.hanada@gmail.com
Hello
here is updated patch, I moved lot of code from lexer to command.com,
and now more \gset doesn't disable other backslash commands on same
line.
Regards
Pavel
2012/10/15 Pavel Stehule <pavel.stehule@gmail.com>:
Show quoted text
2012/10/15 Pavel Stehule <pavel.stehule@gmail.com>:
2012/10/15 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
First of all, I'm sorry that my previous review was rough. I looked
your patch and existing code closely again.On 2012/10/15, at 12:57, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2012/10/14 Tom Lane <tgl@sss.pgh.pa.us>:
* ExecQueryUsingCursor's concept of how to process command results
(why? surely we don't need \gset to use a cursor)There was two possibilities, but hardly non using cursor is better way
+1 for supporting the case when FETCH_COUNT > 0, because user might set
so mainly for other queries, and they would want to avoid changing
FETCH_COUNT setting during every query followed by \gset.* the psql lexer (adding a whole bunch of stuff that probably doesn't
belong there)??
I think that Tom is talking about psql_scan_slash_vars(). It seems too
specific to \gset command. How about to improve
psql_scan_slash_options() so that it can handle comma-separated variable
list? Although you might have tried it before.
# Unused OT_VARLIST macro gave me the idea.yes, it is possible - I'll look on it at evening
a reuse of psql_scan_slash_options is not good idea - a interface of
this function is out of my purposes - and I cannot to signalise error
from this procedure. But I can minimize psql_scan_slash_var and I can
move lot of code out of lexer file.* the core psql settings construct (to store something that is in
no way a persistent setting)??
I thought that having \gset arguments in pset is reasonable, since \g
uses pest.gfname to hold its one-shot setting. Or, we should refactor
\g to fit with \gset? I might be missing Tom's point...Surely there is a less ugly and invasive way to do this. The fact
that the reviewer keeps finding bizarre bugs like "another backslash
command on the same line doesn't work" seems to me to be a good
indication that this is touching things it shouldn't.- all these bugs are based on lexer construct. A little modification
of lexer is possibleIMHO those issues come from the design rather than the implementation of
lexer. AFAIK we don't have consensus about the case that both of \g and
\gset are used for a query like this:postgres=# SELECT 1 \gset var \\ \g foo.txt
This seems regal. Should we store "1" into var and write the result
into foo.txt? Or, only either of them? It's just an idea and it
requires new special character, but how about use \g command for file,
pipe, and variable? In the case we choose '&' for variable prefix:postgres=# SELECT 'hello', 'wonderful', 'world!' \g &var1,,var2
Anyway, we've had no psql's meta command which processes query result
other than \g. So, we might have more considerable issues about design.a current design is rigid - a small implementation can stop parsing
target list, when other backslash statement is detectedBTW, what the word "comma_expected" means? It's in the comment above
psql_scan_slash_vars(). It might be a remaining of old implementation.This identifier is mistaken - etc this comment is wrong and related to
old implementation - sorry. A first design was replaced by state
machine described by VarListParserStateRegards,
--
Shigeru HANADA
shigeru.hanada@gmail.com
Attachments:
gset09.diffapplication/octet-stream; name=gset09.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 71,77 **** static void printSSLInfo(void);
static void checkWin32Codepage(void);
#endif
!
/*----------
* HandleSlashCmds:
--- 71,86 ----
static void checkWin32Codepage(void);
#endif
! /*
! * Possible states for simple state machine, that is used for
! * parsing target list - list of varnames separated by comma.
! */
! typedef enum
! {
! VARLIST_INITIAL,
! VARLIST_EXPECTED_COMMA,
! VARLIST_EXPECTED_COMMA_OR_IDENT
! } VarlistParserState;
/*----------
* HandleSlashCmds:
***************
*** 748,753 **** exec_command(const char *cmd,
--- 757,831 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool value_is_valid;
+ char *value;
+ VarlistParserState state = VARLIST_INITIAL;
+
+ /* expected valid target list */
+ success = true;
+
+ pset.gvars = NULL;
+ while ((value = psql_scan_varlist_varname(scan_state, &value_is_valid)))
+ {
+ if (value_is_valid && success)
+ {
+ if (strcmp(value, ",") == 0)
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+ }
+ else
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ {
+ pset.gvars = tglist_add(pset.gvars, value);
+ state = VARLIST_EXPECTED_COMMA;
+ }
+ else
+ success = false;
+ }
+ }
+ else
+ {
+ success = false;
+ /* but continue, we would to read to eol */
+ }
+
+ free(value);
+ }
+
+ /* final check and target list completation */
+ if (pset.gvars != NULL)
+ {
+ if (success)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT && success)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ status = PSQL_CMD_SEND;
+ }
+ else
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ status = PSQL_CMD_NOSEND;
+
+ }
+ }
+ else
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ success = false;
+ status = PSQL_CMD_NOSEND;
+ }
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1177 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1133,1138 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1238,1246 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1290,1312 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1208,1213 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1332,1339 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1784,1836 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 8,13 ****
--- 8,14 ----
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
+ #include "common.h"
#include "pqexpbuffer.h"
#include "prompt.h"
***************
*** 33,39 **** enum slash_option_type
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL /* no expansion of backticks or variables */
};
--- 34,41 ----
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
! OT_NO_EVAL, /* no expansion of backticks or variables */
! OT_VARLIST /* returns variable's identifier or comma */
};
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 61,69 ----
char *quote,
bool semicolon);
+ extern char *psql_scan_varlist_varname(PsqlScanState state, bool *is_ok);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 167,173 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of varnames
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 184,190 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 630,667 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "," {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "\\\\" {
+ return LEXRES_EOL;
+ }
+
+ "\\" {
+ yyless(0);
+ return LEXRES_EOL;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ ECHO;
+ return LEXRES_EOL;
+ }
+
+ }
+
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1937,1939 **** escape_variable(bool as_ident)
--- 1971,2023 ----
*/
emit(yytext, yyleng);
}
+
+ /*
+ * psql_scan_varlist - scan variable list for \gset command
+ *
+ * Returns next lexem from variable list separated by comma. When
+ * returned string is not empty and out parameter is_ok is false,
+ * then this string contains unexpected char. Returns NULL, when
+ * EOL is detected.
+ */
+ char *
+ psql_scan_varlist_varname(PsqlScanState state, bool *is_ok)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Build a local buffer */
+ initPQExpBuffer(&mybuf);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ if (lexresult == LEXRES_OK)
+ {
+ *is_ok = true;
+ }
+ else if (lexresult == LEXRES_EOL && mybuf.len == 0)
+ {
+ *is_ok = true;
+ termPQExpBuffer(&mybuf);
+ return NULL;
+ }
+ else
+ {
+ *is_ok = false;
+ }
+
+ return mybuf.data;
+ }
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,48 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: missing required argument
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ (1 row)
+
+ 1
+ select 1,2 \gset x,y \echo :x \echo :y
+ (1 row)
+
+ 1
+ 2
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 105,107 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 105,109 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,40 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
+
+ select 1,2 \gset x,y \echo :x \echo :y
Hi Pavel,
On Tue, Oct 16, 2012 at 6:59 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:
here is updated patch, I moved lot of code from lexer to command.com,
and now more \gset doesn't disable other backslash commands on same
line.
* lexer changes
IIUC, new function psql_scan_varlist_varname scans input and returns a
variable name or a comma at each call, and command.c handles the error
such as invalid # of variables. This new design seems better than old one.
However, IMHO the name psql_scan_varlist_varname sounds redundant and
unintuitive. I'd prefer psql_scan_slash_varlist, because it indicates
that that function is expected to be used for arguments of backslash
commands, like psql_scan_slash_command and psql_scan_slash_option.
Thoughts?
* multiple meta command
Now both of the command sequences
$ SELECT 1, 2 \gset var1, var2 \g foo.txt
$ SELECT 1, 2 \g foo.txt \gset var1, var2
set var1 and v2 to "1" and "2" respectively, and also write the result
into foo.txt. This would be what users expected.
* Duplication of variables
I found an issue we have not discussed. Currently \gset accepts same
variable names in the list, and stores last SELECT item in duplicated
variables. For instance,
$ SELECT 1, 2 \gset var, var
stores "2" into var. I think this behavior is acceptable, but it might
be worth mentioning in document.
* extra fixes
I fixed some minor issues below. Please see attached v10 patch for details.
* remove unused macro OT_VARLIST
* remove unnecessary #include directive for common.h
* fill comment within 80 columns
* indent short variable name with tab
* add regression test case for combination of \g and \gset
* bug on FETCH_COUNT = 1
When FETCH_COUNT is set to 1, and the number of rows returned is 1 too,
\gset shows extra "(1 row)". This would be a bug in
ExecQueryUsingCursor. Please see the last test case in regression test
psql_cmd.
I'll mark this patch as "waiting author".
Regards,
--
Shigeru HANADA
Attachments:
gset_10.difftext/x-patch; name=gset_10.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 71,77 **** static void printSSLInfo(void);
static void checkWin32Codepage(void);
#endif
!
/*----------
* HandleSlashCmds:
--- 71,86 ----
static void checkWin32Codepage(void);
#endif
! /*
! * Possible states for simple state machine, that is used for
! * parsing target list - list of varnames separated by comma.
! */
! typedef enum
! {
! VARLIST_INITIAL,
! VARLIST_EXPECTED_COMMA,
! VARLIST_EXPECTED_COMMA_OR_IDENT
! } VarlistParserState;
/*----------
* HandleSlashCmds:
***************
*** 748,753 **** exec_command(const char *cmd,
--- 757,831 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool value_is_valid;
+ char *value;
+ VarlistParserState state = VARLIST_INITIAL;
+
+ /* expected valid target list */
+ success = true;
+
+ pset.gvars = NULL;
+ while ((value = psql_scan_varlist_varname(scan_state, &value_is_valid)))
+ {
+ if (value_is_valid && success)
+ {
+ if (strcmp(value, ",") == 0)
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+ }
+ else
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ {
+ pset.gvars = tglist_add(pset.gvars, value);
+ state = VARLIST_EXPECTED_COMMA;
+ }
+ else
+ success = false;
+ }
+ }
+ else
+ {
+ success = false;
+ /* but continue, we would to read to eol */
+ }
+
+ free(value);
+ }
+
+ /* final check and target list completation */
+ if (pset.gvars != NULL)
+ {
+ if (success)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT && success)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ status = PSQL_CMD_SEND;
+ }
+ else
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ status = PSQL_CMD_NOSEND;
+
+ }
+ }
+ else
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ success = false;
+ status = PSQL_CMD_NOSEND;
+ }
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1177 ----
instr_time before,
after;
int flush_error;
+ bool initial_loop;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1133,1138 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1238,1246 ----
/* clear any pre-existing error indication on the output stream */
clearerr(pset.queryFout);
+ /* we would to allow store result to variables only once */
+ initial_loop = true;
+
for (;;)
{
if (pset.timing)
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1290,1312 ----
did_pager = true;
}
! if (pset.gvars)
! {
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! break;
! }
! }
! else if (store_result && !initial_loop && ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! break;
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1208,1213 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1332,1339 ----
if (ntuples < pset.fetch_count || cancel_pressed || flush_error ||
ferror(pset.queryFout))
break;
+
+ initial_loop = false;
}
/* close \g argument file/pipe, restore old setting */
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1784,1836 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 59,67 ----
char *quote,
bool semicolon);
+ extern char *psql_scan_varlist_varname(PsqlScanState state, bool *is_ok);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 167,173 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of varnames
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 184,190 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 630,667 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "," {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "\\\\" {
+ return LEXRES_EOL;
+ }
+
+ "\\" {
+ yyless(0);
+ return LEXRES_EOL;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ ECHO;
+ return LEXRES_EOL;
+ }
+
+ }
+
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1937,1939 **** escape_variable(bool as_ident)
--- 1971,2022 ----
*/
emit(yytext, yyleng);
}
+
+ /*
+ * psql_scan_varlist - scan variable list for \gset command
+ *
+ * Returns next variable name or comma from variable list. When returned
+ * string is not empty and out parameter is_ok is false, then this string
+ * contains unexpected character. Returns NULL, when EOL is detected.
+ */
+ char *
+ psql_scan_varlist_varname(PsqlScanState state, bool *is_ok)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Build a local buffer */
+ initPQExpBuffer(&mybuf);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ if (lexresult == LEXRES_OK)
+ {
+ *is_ok = true;
+ }
+ else if (lexresult == LEXRES_EOL && mybuf.len == 0)
+ {
+ *is_ok = true;
+ termPQExpBuffer(&mybuf);
+ return NULL;
+ }
+ else
+ {
+ *is_ok = false;
+ }
+
+ return mybuf.data;
+ }
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,64 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: missing required argument
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ (1 row)
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ (1 row)
+
+ 1
+ select 1,2 \gset x,y \echo :x \echo :y
+ (1 row)
+
+ 1
+ 2
+ select 1,2 \gset x,y \\ \g \echo :x :y
+ (1 row)
+
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ select 1,2 \g \gset x,y \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 109,111 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 109,113 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,45 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
+
+ select 1,2 \gset x,y \echo :x \echo :y
+
+ select 1,2 \gset x,y \\ \g \echo :x :y
+
+ select 1,2 \g \gset x,y \echo :x :y
+
2012/10/16 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
On Tue, Oct 16, 2012 at 6:59 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:here is updated patch, I moved lot of code from lexer to command.com,
and now more \gset doesn't disable other backslash commands on same
line.* lexer changes
IIUC, new function psql_scan_varlist_varname scans input and returns a
variable name or a comma at each call, and command.c handles the error
such as invalid # of variables. This new design seems better than old one.However, IMHO the name psql_scan_varlist_varname sounds redundant and
unintuitive. I'd prefer psql_scan_slash_varlist, because it indicates
that that function is expected to be used for arguments of backslash
commands, like psql_scan_slash_command and psql_scan_slash_option.
Thoughts?* multiple meta command
Now both of the command sequences$ SELECT 1, 2 \gset var1, var2 \g foo.txt
$ SELECT 1, 2 \g foo.txt \gset var1, var2set var1 and v2 to "1" and "2" respectively, and also write the result
into foo.txt. This would be what users expected.* Duplication of variables
I found an issue we have not discussed. Currently \gset accepts same
variable names in the list, and stores last SELECT item in duplicated
variables. For instance,$ SELECT 1, 2 \gset var, var
stores "2" into var. I think this behavior is acceptable, but it might
be worth mentioning in document.* extra fixes
I fixed some minor issues below. Please see attached v10 patch for details.* remove unused macro OT_VARLIST
* remove unnecessary #include directive for common.h
* fill comment within 80 columns
* indent short variable name with tab
* add regression test case for combination of \g and \gset* bug on FETCH_COUNT = 1
When FETCH_COUNT is set to 1, and the number of rows returned is 1 too,
\gset shows extra "(1 row)". This would be a bug in
ExecQueryUsingCursor. Please see the last test case in regression test
psql_cmd.
I fixed this bug
Regards
Pavel
Show quoted text
I'll mark this patch as "waiting author".
Regards,
--
Shigeru HANADA
Attachments:
gset_11.diffapplication/octet-stream; name=gset_11.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1644 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 71,77 **** static void printSSLInfo(void);
static void checkWin32Codepage(void);
#endif
!
/*----------
* HandleSlashCmds:
--- 71,86 ----
static void checkWin32Codepage(void);
#endif
! /*
! * Possible states for simple state machine, that is used for
! * parsing target list - list of varnames separated by comma.
! */
! typedef enum
! {
! VARLIST_INITIAL,
! VARLIST_EXPECTED_COMMA,
! VARLIST_EXPECTED_COMMA_OR_IDENT
! } VarlistParserState;
/*----------
* HandleSlashCmds:
***************
*** 748,753 **** exec_command(const char *cmd,
--- 757,831 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ bool value_is_valid;
+ char *value;
+ VarlistParserState state = VARLIST_INITIAL;
+
+ /* expected valid target list */
+ success = true;
+
+ pset.gvars = NULL;
+ while ((value = psql_scan_varlist_varname(scan_state, &value_is_valid)))
+ {
+ if (value_is_valid && success)
+ {
+ if (strcmp(value, ",") == 0)
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+ }
+ else
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ {
+ pset.gvars = tglist_add(pset.gvars, value);
+ state = VARLIST_EXPECTED_COMMA;
+ }
+ else
+ success = false;
+ }
+ }
+ else
+ {
+ success = false;
+ /* but continue, we would to read to eol */
+ }
+
+ free(value);
+ }
+
+ /* final check and target list completation */
+ if (pset.gvars != NULL)
+ {
+ if (success)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT && success)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ status = PSQL_CMD_SEND;
+ }
+ else
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ status = PSQL_CMD_NOSEND;
+
+ }
+ }
+ else
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ success = false;
+ status = PSQL_CMD_NOSEND;
+ }
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,919 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ if (!SetVariable(pset.vars, iter->name,
+ PQgetvalue(result, 0, i)))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1041,1052 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1170,1176 ----
instr_time before,
after;
int flush_error;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1182,1191 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
/* after the first result set, disallow header decoration */
my_popt.topt.start_table = false;
my_popt.topt.prior_records += ntuples;
--- 1286,1320 ----
did_pager = true;
}
! if (pset.gvars)
! {
! /* initial iteration when tartget are variables */
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! break;
! }
! }
! else if (store_result)
! {
! /* must be second loop */
! if (ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! OK = false;
! break;
! }
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
+
/* after the first result set, disallow header decoration */
my_popt.topt.start_table = false;
my_popt.topt.prior_records += ntuples;
***************
*** 1254,1259 **** cleanup:
--- 1383,1389 ----
PQclear(results);
}
+
if (pset.timing)
{
INSTR_TIME_SET_CURRENT(after);
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1788,1840 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/psqlscan.h
--- b/src/bin/psql/psqlscan.h
***************
*** 59,64 **** extern char *psql_scan_slash_option(PsqlScanState state,
--- 59,67 ----
char *quote,
bool semicolon);
+ extern char *psql_scan_varlist_varname(PsqlScanState state, bool *is_ok);
+
+
extern void psql_scan_slash_command_end(PsqlScanState state);
#endif /* PSQLSCAN_H */
*** a/src/bin/psql/psqlscan.l
--- b/src/bin/psql/psqlscan.l
***************
*** 167,172 **** static void escape_variable(bool as_ident);
--- 167,173 ----
* <xdolq> $foo$ quoted strings
* <xui> quoted identifier with Unicode escapes
* <xus> quoted string with Unicode escapes
+ * <xvl> comma separated list of varnames
*
* Note: we intentionally don't mimic the backend's <xeu> state; we have
* no need to distinguish it from <xe> state, and no good way to get out
***************
*** 183,188 **** static void escape_variable(bool as_ident);
--- 184,190 ----
%x xdolq
%x xui
%x xus
+ %x xvl
/* Additional exclusive states for psql only: lex backslash commands */
%x xslashcmd
%x xslashargstart
***************
*** 628,633 **** other .
--- 630,667 ----
ECHO;
}
+ <xvl>{
+
+ {identifier} {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "," {
+ ECHO;
+ return LEXRES_OK;
+ }
+
+ "\\\\" {
+ return LEXRES_EOL;
+ }
+
+ "\\" {
+ yyless(0);
+ return LEXRES_EOL;
+ }
+
+ {horiz_space}+ { }
+
+ . |
+ \n {
+ ECHO;
+ return LEXRES_EOL;
+ }
+
+ }
+
+
{xufailed} {
/* throw back all but the initial u/U */
yyless(1);
***************
*** 1937,1939 **** escape_variable(bool as_ident)
--- 1971,2022 ----
*/
emit(yytext, yyleng);
}
+
+ /*
+ * psql_scan_varlist - scan variable list for \gset command
+ *
+ * Returns next variable name or comma from variable list. When returned
+ * string is not empty and out parameter is_ok is false, then this string
+ * contains unexpected character. Returns NULL, when EOL is detected.
+ */
+ char *
+ psql_scan_varlist_varname(PsqlScanState state, bool *is_ok)
+ {
+ PQExpBufferData mybuf;
+ int lexresult;
+
+ /* Must be scanning already */
+ psql_assert(state->scanbufhandle);
+
+ /* Build a local buffer */
+ initPQExpBuffer(&mybuf);
+
+ /* Set up static variables that will be used by yylex */
+ cur_state = state;
+ output_buf = &mybuf;
+
+ if (state->buffer_stack != NULL)
+ yy_switch_to_buffer(state->buffer_stack->buf);
+ else
+ yy_switch_to_buffer(state->scanbufhandle);
+
+ BEGIN(xvl);
+ lexresult = yylex();
+
+ if (lexresult == LEXRES_OK)
+ {
+ *is_ok = true;
+ }
+ else if (lexresult == LEXRES_EOL && mybuf.len == 0)
+ {
+ *is_ok = true;
+ termPQExpBuffer(&mybuf);
+ return NULL;
+ }
+ else
+ {
+ *is_ok = false;
+ }
+
+ return mybuf.data;
+ }
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 9,14 ****
--- 9,15 ----
#define SETTINGS_H
+ #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 74,80 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,56 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ \gset ,, -- error
+ \gset: syntax error
+ \gset , -- error
+ \gset: syntax error
+ \gset ,,, -- error
+ \gset: syntax error
+ \gset -- error
+ \gset: missing required argument
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ 1
+ select 1,2 \gset x,y \echo :x \echo :y
+ 1
+ 2
+ select 1,2 \gset x,y \\ \g \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ select 1,2 \g \gset x,y \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 109,111 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 109,113 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,45 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ \gset ,, -- error
+ \gset , -- error
+ \gset ,,, -- error
+ \gset -- error
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
+
+ select 1,2 \gset x,y \echo :x \echo :y
+
+ select 1,2 \gset x,y \\ \g \echo :x :y
+
+ select 1,2 \g \gset x,y \echo :x :y
+
On Tue, Oct 16, 2012 at 12:24 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
2012/10/16 Shigeru HANADA <shigeru.hanada@gmail.com>:
Hi Pavel,
On Tue, Oct 16, 2012 at 6:59 AM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:here is updated patch, I moved lot of code from lexer to command.com,
and now more \gset doesn't disable other backslash commands on same
line.* lexer changes
IIUC, new function psql_scan_varlist_varname scans input and returns a
variable name or a comma at each call, and command.c handles the error
such as invalid # of variables. This new design seems better than old one.However, IMHO the name psql_scan_varlist_varname sounds redundant and
unintuitive. I'd prefer psql_scan_slash_varlist, because it indicates
that that function is expected to be used for arguments of backslash
commands, like psql_scan_slash_command and psql_scan_slash_option.
Thoughts?* multiple meta command
Now both of the command sequences$ SELECT 1, 2 \gset var1, var2 \g foo.txt
$ SELECT 1, 2 \g foo.txt \gset var1, var2set var1 and v2 to "1" and "2" respectively, and also write the result
into foo.txt. This would be what users expected.* Duplication of variables
I found an issue we have not discussed. Currently \gset accepts same
variable names in the list, and stores last SELECT item in duplicated
variables. For instance,$ SELECT 1, 2 \gset var, var
stores "2" into var. I think this behavior is acceptable, but it might
be worth mentioning in document.* extra fixes
I fixed some minor issues below. Please see attached v10 patch for details.* remove unused macro OT_VARLIST
* remove unnecessary #include directive for common.h
* fill comment within 80 columns
* indent short variable name with tab
* add regression test case for combination of \g and \gset* bug on FETCH_COUNT = 1
When FETCH_COUNT is set to 1, and the number of rows returned is 1 too,
\gset shows extra "(1 row)". This would be a bug in
ExecQueryUsingCursor. Please see the last test case in regression test
psql_cmd.I fixed this bug
Regards
Pavel
I'll mark this patch as "waiting author".
Regards,
--
Shigeru HANADA--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
My first review...
Patch applied cleanly to master and make check was fine.
I tested it out and it operates as advertised. There were a couple
things that stood out to me though.
1) NULL values are not displayed properly after \pset null is run.
postgres=# \pset null '(null)'
Null display is "(null)".
postgres=# select NULL \gset var1
postgres=# \echo :var1
postgres=# select NULL;
?column?
----------
(null)
(1 row)
I know this doesn't come back from the server like this, but you
should be able to pull this from the options and display
appropriately. Not sure if it should be in variable display code, or
when you store it into the variable.
2) The error messages seemed kind of terse. Other error messages are
capitalized and have punctuation.
3) We don't find out about incorrect number of columns until after
query returns. I know this is hard/impossible to fix, but it might be
nice to print out the result normally if you can't store it in the
variables.
3b) You throw an error on too many variables, but still store the data
since you have fewer columns than variables. This makes sense, but you
don't inform the user at all.
On to the code:
1) Introduction of random newlines:
*************** cleanup:
*** 1254,1259 ****
--- 1383,1389 ----
PQclear(results);
}
+
if (pset.timing)
{
INSTR_TIME_SET_CURRENT(after);
I saw that in a couple places, but that was the most obvious.
2) TargetList - Why not use the built in linked list operations rather
than creating your own? Are they not accessible to client binaries
like this?
Overall I think this is a useful feature and I think you integrated it
well within the existing infrastructure, ie combining concepts of \set
and \g.
I gave this a look. I think it needs to be revised by somebody with a
better understanding of scanner (flex) than me, but I didn't like the
changes in psqlscan.l at all; the new <xvl> pattern is too unlike the
rest of the surrounding patterns, and furthermore it has been placed
within the block that says it mirrors the backend scanner, when it
obviously has no equivalent there.
I assume there's a better way to do this. Hints would be appreciated.
--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
I gave this a look. I think it needs to be revised by somebody with a
better understanding of scanner (flex) than me, but I didn't like the
changes in psqlscan.l at all; the new <xvl> pattern is too unlike the
rest of the surrounding patterns, and furthermore it has been placed
within the block that says it mirrors the backend scanner, when it
obviously has no equivalent there.
I assume there's a better way to do this. Hints would be appreciated.
Personally I saw no reason for this patch to touch psqlscan.l in the
first place. Commands such as \set just scan variable names with
psql_scan_slash_option(OT_NORMAL); why shouldn't this act the same?
Moreover, the proposed lexer rules are flat out *wrong*, in that they
insist on the target variable names being {identifier}s, a restriction
not imposed by \set.
regards, tom lane
Tom Lane escribió:
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
I gave this a look. I think it needs to be revised by somebody with a
better understanding of scanner (flex) than me, but I didn't like the
changes in psqlscan.l at all; the new <xvl> pattern is too unlike the
rest of the surrounding patterns, and furthermore it has been placed
within the block that says it mirrors the backend scanner, when it
obviously has no equivalent there.I assume there's a better way to do this. Hints would be appreciated.
Personally I saw no reason for this patch to touch psqlscan.l in the
first place. Commands such as \set just scan variable names with
psql_scan_slash_option(OT_NORMAL); why shouldn't this act the same?Moreover, the proposed lexer rules are flat out *wrong*, in that they
insist on the target variable names being {identifier}s, a restriction
not imposed by \set.
Great, thanks for the feedback. Marking as returned in CF. I hope to
see a new version after pgconf.eu.
--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
2012/10/25 Tom Lane <tgl@sss.pgh.pa.us>:
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
I gave this a look. I think it needs to be revised by somebody with a
better understanding of scanner (flex) than me, but I didn't like the
changes in psqlscan.l at all; the new <xvl> pattern is too unlike the
rest of the surrounding patterns, and furthermore it has been placed
within the block that says it mirrors the backend scanner, when it
obviously has no equivalent there.I assume there's a better way to do this. Hints would be appreciated.
Personally I saw no reason for this patch to touch psqlscan.l in the
first place. Commands such as \set just scan variable names with
psql_scan_slash_option(OT_NORMAL); why shouldn't this act the same?
it cannot be same, because current scan doesn't know comma as
separator. So if you don't like changes in scanner, than we can't to
use "var1, var2," syntax and we can't to use leaky list syntax ",x,"
Moreover, the proposed lexer rules are flat out *wrong*, in that they
insist on the target variable names being {identifier}s, a restriction
not imposed by \set.
do you like to support referenced varnames??
postgres=# \varname xxx
Invalid command \varname. Try \? for help.
postgres=# \set varname xxx
postgres=# \set :varname Hello
postgres=# \set
varname = 'xxx'
xxx = 'Hello'
yes, \set support it, but this can be source of "strange behave" for
some people, because people use :varname like $varname in classic
scripting languages, and it is significantly different - so I didn't
support it as little bit dangerous feature. It is easy support it,
although I am thinking, so it is not good idea, because behave is
really different than users expect and I don't know any use case for
this indirect referencing. But I would to talk about it, and I invite
opinion of others.
Regards
Pavel
Show quoted text
regards, tom lane
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/10/25 Tom Lane <tgl@sss.pgh.pa.us>:
Personally I saw no reason for this patch to touch psqlscan.l in the
first place. Commands such as \set just scan variable names with
psql_scan_slash_option(OT_NORMAL); why shouldn't this act the same?
it cannot be same, because current scan doesn't know comma as
separator. So if you don't like changes in scanner, than we can't to
use "var1, var2," syntax and we can't to use leaky list syntax ",x,"
Uh, no, that doesn't follow. It wouldn't be any more code to have
command.c process the commas (or even more likely, just save the \gset
argument(s) as a string, and split on commas after we've done the
command). Even if we wanted to do that in psqlscan.l, this was a pretty
bad/ugly implementation of it.
Moreover, the proposed lexer rules are flat out *wrong*, in that they
insist on the target variable names being {identifier}s, a restriction
not imposed by \set.
yes, \set support it, but this can be source of "strange behave" for
some people, because people use :varname like $varname in classic
scripting languages, and it is significantly different - so I didn't
support it as little bit dangerous feature.
[ shrug... ] If you want to argue for imposing a restriction on
psql variable names across-the-board, we could have that discussion;
but personally I've not seen even one user complaint that could be
traced to \set's laxity in the matter, so I don't see a need for
a restriction. In any case, having \gset enforce a restriction
that \set doesn't is useless and inconsistent.
regards, tom lane
2012/10/25 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
2012/10/25 Tom Lane <tgl@sss.pgh.pa.us>:
Personally I saw no reason for this patch to touch psqlscan.l in the
first place. Commands such as \set just scan variable names with
psql_scan_slash_option(OT_NORMAL); why shouldn't this act the same?it cannot be same, because current scan doesn't know comma as
separator. So if you don't like changes in scanner, than we can't to
use "var1, var2," syntax and we can't to use leaky list syntax ",x,"Uh, no, that doesn't follow. It wouldn't be any more code to have
command.c process the commas (or even more likely, just save the \gset
argument(s) as a string, and split on commas after we've done the
command). Even if we wanted to do that in psqlscan.l, this was a pretty
bad/ugly implementation of it.
I don't understand, why we have to move lexer work from scanner to
command processing?
then I afraid of another issue - when we do late separation in command
somebody can do
\set targetvars a,b,c
select xxxx
\gset x1,x2,:targetvars,x3
We would to do this? Then we moving to TeX liked languages. I am asking.
Moreover, the proposed lexer rules are flat out *wrong*, in that they
insist on the target variable names being {identifier}s, a restriction
not imposed by \set.yes, \set support it, but this can be source of "strange behave" for
some people, because people use :varname like $varname in classic
scripting languages, and it is significantly different - so I didn't
support it as little bit dangerous feature.[ shrug... ] If you want to argue for imposing a restriction on
psql variable names across-the-board, we could have that discussion;
but personally I've not seen even one user complaint that could be
traced to \set's laxity in the matter, so I don't see a need for
a restriction. In any case, having \gset enforce a restriction
that \set doesn't is useless and inconsistent.
ok, it has a sense
Show quoted text
regards, tom lane
Hello
My first review...
Patch applied cleanly to master and make check was fine.
I tested it out and it operates as advertised. There were a couple
things that stood out to me though.1) NULL values are not displayed properly after \pset null is run.
postgres=# \pset null '(null)'
Null display is "(null)".
postgres=# select NULL \gset var1
postgres=# \echo :var1postgres=# select NULL;
?column?
----------
(null)
(1 row)I know this doesn't come back from the server like this, but you
should be able to pull this from the options and display
appropriately. Not sure if it should be in variable display code, or
when you store it into the variable.
ok
2) The error messages seemed kind of terse. Other error messages are
capitalized and have punctuation.
ok
3) We don't find out about incorrect number of columns until after
query returns. I know this is hard/impossible to fix, but it might be
nice to print out the result normally if you can't store it in the
variables.
a changing behave when error is occurred is not good, but I can show a
number of returned columns
3b) You throw an error on too many variables, but still store the data
since you have fewer columns than variables. This makes sense, but you
don't inform the user at all.
there is not a some like stack, so I cannot to return values to
previous state. It should be documented - after error, a related
variables has undefined values.
On to the code:
1) Introduction of random newlines:
*************** cleanup: *** 1254,1259 **** --- 1383,1389 ---- PQclear(results); }+
if (pset.timing)
{
INSTR_TIME_SET_CURRENT(after);I saw that in a couple places, but that was the most obvious.
2) TargetList - Why not use the built in linked list operations rather
than creating your own? Are they not accessible to client binaries
like this?
actually, there are no support for lists on client side now. So it is
reason. But I'll remove it, because I'll move parsing to command
implementation, and then I don't need a list support
Overall I think this is a useful feature and I think you integrated it
well within the existing infrastructure, ie combining concepts of \set
and \g.
Thank you
Regards
Pavel
Hello
here is updated patch
main change - it doesn't touch psql lexer - like Tom proposed
other points respect Phil's notices
My first review...
Patch applied cleanly to master and make check was fine.
I tested it out and it operates as advertised. There were a couple
things that stood out to me though.1) NULL values are not displayed properly after \pset null is run.
postgres=# \pset null '(null)'
Null display is "(null)".
postgres=# select NULL \gset var1
postgres=# \echo :var1postgres=# select NULL;
?column?
----------
(null)
(1 row)I know this doesn't come back from the server like this, but you
should be able to pull this from the options and display
appropriately. Not sure if it should be in variable display code, or
when you store it into the variable.
fixed and add to regress test
2) The error messages seemed kind of terse. Other error messages are
capitalized and have punctuation.
After some thinking I didn't change it - it is consistent with other
messages in psql - short messages that are not complete sentence are
no capitalized and have not punctuation like other short messages in
psql
3) We don't find out about incorrect number of columns until after
query returns. I know this is hard/impossible to fix, but it might be
nice to print out the result normally if you can't store it in the
variables.
I didn't change it because a) I don't think so change behave after
error is good idea, b) \gset doesn't remove SQL from query buffer, so
you can repeat it
postgres=> select 10,20,30
postgres-> \gset a,b
too few target variables
postgres=> \g
?column? │ ?column? │ ?column?
──────────┼──────────┼──────────
10 │ 20 │ 30
(1 row)
postgres=> \gset a,b,c
3b) You throw an error on too many variables, but still store the data
since you have fewer columns than variables. This makes sense, but you
don't inform the user at all.
it is commented in doc
+ <para>
+ When this command fails, then related <replaceable
+ class="parameter">variables</replaceable> has undefined content.
+ </para>
On to the code:
1) Introduction of random newlines:
*************** cleanup: *** 1254,1259 **** --- 1383,1389 ---- PQclear(results); }+
if (pset.timing)
{
INSTR_TIME_SET_CURRENT(after);I saw that in a couple places, but that was the most obvious.
I hope so I moved to /dev/null all
2) TargetList - Why not use the built in linked list operations rather
than creating your own? Are they not accessible to client binaries
like this?
There was not support for lists on client part to today. So I had to
created own simple implementation.
Overall I think this is a useful feature and I think you integrated it
well within the existing infrastructure, ie combining concepts of \set
and \g.
Thank you very much again
Regards
Pavel
Attachments:
gset_12.diffapplication/octet-stream; name=gset_12.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1648 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ <para>
+ When this command fails, then related <replaceable
+ class="parameter">variables</replaceable> has undefined content.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 71,77 **** static void printSSLInfo(void);
static void checkWin32Codepage(void);
#endif
!
/*----------
* HandleSlashCmds:
--- 71,86 ----
static void checkWin32Codepage(void);
#endif
! /*
! * Possible states for simple state machine, that is used for
! * parsing target list - list of varnames separated by comma.
! */
! typedef enum
! {
! VARLIST_INITIAL,
! VARLIST_EXPECTED_COMMA,
! VARLIST_EXPECTED_COMMA_OR_IDENT
! } VarlistParserState;
/*----------
* HandleSlashCmds:
***************
*** 748,753 **** exec_command(const char *cmd,
--- 757,870 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ char *packet;
+ VarlistParserState state = VARLIST_INITIAL;
+
+ /* expected valid target list */
+ success = true;
+ pset.gvars = NULL;
+
+ /*
+ * So we would to enable subtitution, but we know so target varname should
+ * not use comma. Recheck of variable name is done by variable setting routine.
+ * psql_scan_slash_option can returns zero or more identifiers separated by
+ * comma. We cennot to use OT_WHOLE_LINE, because variable substitution is not
+ * supported there.
+ */
+ while ((packet = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false)))
+ {
+ char *token = packet;
+ bool process_comma = false;
+
+ while (success && (*token || process_comma))
+ {
+ if (process_comma)
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+
+ process_comma = false;
+ }
+ else
+ {
+ char *ptr = token;
+
+ /* skip initial blank chars */
+ while (*ptr && isblank(*ptr))
+ ptr++;
+
+ token = ptr;
+
+ /* search separator */
+ while (*ptr && *ptr != ',')
+ ptr++;
+
+ /* replace separator by zero char */
+ if (*ptr == ',')
+ {
+ process_comma = true;
+ *ptr = '\0';
+ }
+
+ /* process token when it is non empty string */
+ if (token != ptr)
+ {
+ if (state == VARLIST_INITIAL ||
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ {
+ pset.gvars = tglist_add(pset.gvars, token);
+ state = VARLIST_EXPECTED_COMMA;
+ }
+ else
+ {
+ success = false;
+ break;
+ }
+ }
+
+ /* move pointer to next token when comma is found */
+ if (process_comma)
+ token = ptr + 1;
+ else
+ /* leave when there are no next token */
+ break;
+ }
+ }
+ free(packet);
+ }
+
+ /* final check and target list completation */
+ if (pset.gvars != NULL)
+ {
+ if (success)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ status = PSQL_CMD_SEND;
+ }
+ else
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ status = PSQL_CMD_NOSEND;
+
+ }
+ }
+ else
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ success = false;
+ status = PSQL_CMD_NOSEND;
+ }
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 816,821 **** PrintQueryResults(PGresult *results)
--- 816,925 ----
return success;
}
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ {
+ int i;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+
+ success = true;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ char *value;
+
+ if (PQgetisnull(result, 0, i))
+ value = pset.popt.nullPrint ? pset.popt.nullPrint : "";
+ else
+ value = PQgetvalue(result, 0, i);
+
+ if (!SetVariable(pset.vars, iter->name, value))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+ }
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
/*
* SendQuery: send the query string to the backend
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1047,1058 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1176,1182 ----
instr_time before,
after;
int flush_error;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1292,1322 ----
did_pager = true;
}
! if (pset.gvars)
! {
! /* initial iteration when tartget are variables */
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! break;
! }
! }
! else if (store_result)
! {
! /* must be second loop */
! if (ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! OK = false;
! break;
! }
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1792,1844 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 21,26 ****
--- 21,34 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 62,65 **** extern const char *session_username(void);
--- 70,76 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 8,14 ****
#ifndef SETTINGS_H
#define SETTINGS_H
!
#include "variables.h"
#include "print.h"
--- 8,14 ----
#ifndef SETTINGS_H
#define SETTINGS_H
! #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 73,79 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,68 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ -- errors
+ \gset ,,
+ \gset ,
+ too few target variables
+ \gset ,,,
+ too many target variables
+ \gset
+ \gset: missing required argument
+ -- using badly varnames - detected when variable is accessed
+ -- note: row comments are not supported by psql lexer when command option
+ -- is read - so these statements finish by syntax error - not expected
+ -- "missing required argument" error.
+ \set --varname 10
+ \set: error while setting variable
+ \gset --varname1, varname2, varname3
+ invalid variable name: "--varname1"
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ 1
+ select 1,2 \gset x,y \echo :x \echo :y
+ 1
+ 2
+ select 1,2 \gset x,y \\ \g \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ select 1,2 \g \gset x,y \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ \pset null '(null)'
+ select NULL \gset var1
+ \echo :var1
+ (null)
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 109,111 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 109,113 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 134,136 **** test: largeobject
--- 134,137 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,57 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ -- errors
+ \gset ,,
+ \gset ,
+ \gset ,,,
+ \gset
+
+ -- using badly varnames - detected when variable is accessed
+ -- note: row comments are not supported by psql lexer when command option
+ -- is read - so these statements finish by syntax error - not expected
+ -- "missing required argument" error.
+ \set --varname 10
+ \gset --varname1, varname2, varname3
+
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
+
+ select 1,2 \gset x,y \echo :x \echo :y
+
+ select 1,2 \gset x,y \\ \g \echo :x :y
+
+ select 1,2 \g \gset x,y \echo :x :y
+
+ \pset null '(null)'
+ select NULL \gset var1
+ \echo :var1
On Sun, Oct 28, 2012 at 7:16 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Hello
here is updated patch
main change - it doesn't touch psql lexer - like Tom proposed
other points respect Phil's notices
I reviewed v12 patch. It provides gset command which works
consistently with other psql commands, such as \g and \set, and
implementation seems reasonable, and follows other reviewer's comments
properly. I think we can mark it as "ready for committer", once you
have fixed some minor issues below.
* Skipping leading blank in inner while loop of command.c seems
unnecessary, because (IIUC) psql's scanner skips blanks. Is there any
case that scanner returns token with leading/trailing blank?
* ISTM that VARLIST_INITIAL can be removed. AFAIS it's same state as
VARLIST_EXPECTED_COMMA_OR_IDENT.
* I found some cosmetic flaw and typo. Please see attached patch for details.
* How about pulling up codes for PGRES_TUPLES_OK case in
StoreQueryResult to new static function, say StoreQueryTuple? It
would make StoreQueryResult more similar to PrintQueryResult's style,
and IMO it makes the code more readable.
Regards,
--
Shigeru HANADA
Attachments:
gset_fix.patchapplication/octet-stream; name=gset_fix.patchDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 645ba5b..d36e5de 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -71,6 +71,7 @@ static void printSSLInfo(void);
static void checkWin32Codepage(void);
#endif
+
/*
* Possible states for simple state machine, that is used for
* parsing target list - list of varnames separated by comma.
@@ -760,7 +761,7 @@ exec_command(const char *cmd,
/* \gset send query and store result */
else if (strcmp(cmd, "gset") == 0)
{
- char *packet;
+ char *packet;
VarlistParserState state = VARLIST_INITIAL;
/* expected valid target list */
@@ -768,32 +769,31 @@ exec_command(const char *cmd,
pset.gvars = NULL;
/*
- * So we would to enable subtitution, but we know so target varname should
- * not use comma. Recheck of variable name is done by variable setting routine.
- * psql_scan_slash_option can returns zero or more identifiers separated by
- * comma. We cennot to use OT_WHOLE_LINE, because variable substitution is not
- * supported there.
+ * So we would to enable substitution, but we know so target varname
+ * should not use comma. Recheck of variable name is done by variable
+ * setting routine. psql_scan_slash_option can returns zero or more
+ * identifiers separated by comma. We cannot to use OT_WHOLE_LINE,
+ * because variable substitution is not supported there.
*/
while ((packet = psql_scan_slash_option(scan_state,
- OT_NORMAL, NULL, false)))
+ OT_NORMAL, NULL, false)))
{
- char *token = packet;
- bool process_comma = false;
+ char *token = packet;
+ bool process_comma = false;
while (success && (*token || process_comma))
{
if (process_comma)
{
if (state == VARLIST_INITIAL ||
- state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
pset.gvars = tglist_add(pset.gvars, NULL);
state = VARLIST_EXPECTED_COMMA_OR_IDENT;
-
process_comma = false;
}
else
{
- char *ptr = token;
+ char *ptr = token;
/* skip initial blank chars */
while (*ptr && isblank(*ptr))
@@ -816,7 +816,7 @@ exec_command(const char *cmd,
if (token != ptr)
{
if (state == VARLIST_INITIAL ||
- state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ state == VARLIST_EXPECTED_COMMA_OR_IDENT)
{
pset.gvars = tglist_add(pset.gvars, token);
state = VARLIST_EXPECTED_COMMA;
2012/12/17 Shigeru Hanada <shigeru.hanada@gmail.com>:
On Sun, Oct 28, 2012 at 7:16 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Hello
here is updated patch
main change - it doesn't touch psql lexer - like Tom proposed
other points respect Phil's noticesI reviewed v12 patch. It provides gset command which works
consistently with other psql commands, such as \g and \set, and
implementation seems reasonable, and follows other reviewer's comments
properly. I think we can mark it as "ready for committer", once you
have fixed some minor issues below.* Skipping leading blank in inner while loop of command.c seems
unnecessary, because (IIUC) psql's scanner skips blanks. Is there any
case that scanner returns token with leading/trailing blank?
removed
* ISTM that VARLIST_INITIAL can be removed. AFAIS it's same state as
VARLIST_EXPECTED_COMMA_OR_IDENT.
removed
* I found some cosmetic flaw and typo. Please see attached patch for details.
it is ok, merged
* How about pulling up codes for PGRES_TUPLES_OK case in
StoreQueryResult to new static function, say StoreQueryTuple? It
would make StoreQueryResult more similar to PrintQueryResult's style,
and IMO it makes the code more readable.
good idea
done
Attached updated patch
Regards
Pavel
Show quoted text
Regards,
--
Shigeru HANADA
Attachments:
gset_13.diffapplication/octet-stream; name=gset_13.diffDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1483,1490 **** testdb=>
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon or <literal>\g</> to send it, or
! <literal>\r</> to cancel.
</para>
<para>
--- 1483,1490 ----
way. Use <command>\i</command> for that.) This means that
if the query ends with (or contains) a semicolon, it is
immediately executed. Otherwise it will merely wait in the
! query buffer; type semicolon, <literal>\g</> or
! <literal>\gset</literal> to send it, or <literal>\r</> to cancel.
</para>
<para>
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1648 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">variable</replaceable> [ ,<replaceable class="parameter">variable</replaceable> ... ] </term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row, and the number of variables must be same as the
+ number of elements in <command>SELECT</command> list. If you don't
+ need any of items in <command>SELECT</command> list, you can omit
+ corresponding <replaceable class="parameter">variable</replaceable>.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello', 'wonderful', 'world!' \gset var1,,var3
+ foo=> \echo :var1 :var3
+ hello world!
+ </programlisting>
+ </para>
+ <para>
+ When this command fails, then related <replaceable
+ class="parameter">variables</replaceable> has undefined content.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 72,77 **** static void checkWin32Codepage(void);
--- 72,86 ----
#endif
+ /*
+ * Possible states for simple state machine, that is used for
+ * parsing target list - list of varnames separated by comma.
+ */
+ typedef enum
+ {
+ VARLIST_EXPECTED_COMMA,
+ VARLIST_EXPECTED_COMMA_OR_IDENT
+ } VarlistParserState;
/*----------
* HandleSlashCmds:
***************
*** 748,753 **** exec_command(const char *cmd,
--- 757,861 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ char *packet;
+ VarlistParserState state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+
+ /* expected valid target list */
+ success = true;
+ pset.gvars = NULL;
+
+ /*
+ * So we would to enable substitution, but we know so target varname
+ * should not use comma. Recheck of variable name is done by variable
+ * setting routine. psql_scan_slash_option can returns zero or more
+ * identifiers separated by comma. We cannot to use OT_WHOLE_LINE,
+ * because variable substitution is not supported there.
+ */
+ while ((packet = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false)))
+ {
+ char *token = packet;
+ bool process_comma = false;
+
+ while (success && (*token || process_comma))
+ {
+ if (process_comma)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ state = VARLIST_EXPECTED_COMMA_OR_IDENT;
+ process_comma = false;
+ }
+ else
+ {
+ char *ptr = token;
+
+ /* search separator */
+ while (*ptr && *ptr != ',')
+ ptr++;
+
+ /* replace separator by zero char */
+ if (*ptr == ',')
+ {
+ process_comma = true;
+ *ptr = '\0';
+ }
+
+ /* process token when it is non empty string */
+ if (token != ptr)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ {
+ pset.gvars = tglist_add(pset.gvars, token);
+ state = VARLIST_EXPECTED_COMMA;
+ }
+ else
+ {
+ success = false;
+ break;
+ }
+ }
+
+ /* move pointer to next token when comma is found */
+ if (process_comma)
+ token = ptr + 1;
+ else
+ /* leave when there are no next token */
+ break;
+ }
+ }
+ free(packet);
+ }
+
+ /* final check and target list completation */
+ if (pset.gvars != NULL)
+ {
+ if (success)
+ {
+ if (state == VARLIST_EXPECTED_COMMA_OR_IDENT)
+ pset.gvars = tglist_add(pset.gvars, NULL);
+ status = PSQL_CMD_SEND;
+ }
+ else
+ {
+ psql_error("\\%s: syntax error\n", cmd);
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+ status = PSQL_CMD_NOSEND;
+
+ }
+ }
+ else
+ {
+ psql_error("\\%s: missing required argument\n", cmd);
+ success = false;
+ status = PSQL_CMD_NOSEND;
+ }
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/command.h
--- b/src/bin/psql/command.h
***************
*** 16,21 **** typedef enum _backslashResult
--- 16,22 ----
{
PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
PSQL_CMD_SEND, /* query complete; send off */
+ PSQL_CMD_NOSEND, /* query complete, don't send */
PSQL_CMD_SKIP_LINE, /* keep building query */
PSQL_CMD_TERMINATE, /* quit program */
PSQL_CMD_NEWEDIT, /* query buffer was changed (e.g., via \e) */
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 818,823 **** PrintQueryResults(PGresult *results)
--- 818,938 ----
/*
+ * StoreQueryTuple: assuming query result is OK, save first tuple
+ *
+ * Returns true if successful, false otherwise.
+ */
+ static bool
+ StoreQueryTuple(PGresult *result)
+ {
+ bool success = true;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ TargetListData *iter = (TargetListData *) pset.gvars;
+ int i;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ if (!iter)
+ {
+ psql_error("too few target variables\n");
+ success = false;
+ break;
+ }
+
+ if (iter->name)
+ {
+ char *value;
+
+ if (PQgetisnull(result, 0, i))
+ value = pset.popt.nullPrint ? pset.popt.nullPrint : "";
+ else
+ value = PQgetvalue(result, 0, i);
+
+ if (!SetVariable(pset.vars, iter->name, value))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ iter->name);
+ success = false;
+ break;
+ }
+ }
+
+ iter = iter->next;
+ }
+
+ if (success && iter != NULL)
+ {
+ psql_error("too many target variables\n");
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ success = StoreQueryTuple(result);
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ tglist_free(pset.gvars);
+ pset.gvars = NULL;
+
+ return success;
+ }
+
+ /*
* SendQuery: send the query string to the backend
* (and print out results)
*
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1058,1069 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvars)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1187,1193 ----
instr_time before,
after;
int flush_error;
+ bool store_result = pset.gvars != NULL;
*elapsed_msec = 0;
***************
*** 1182,1188 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
did_pager = true;
}
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
--- 1303,1333 ----
did_pager = true;
}
! if (pset.gvars)
! {
! /* initial iteration when tartget are variables */
! OK = StoreQueryResult(results);
! if (!OK)
! {
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! break;
! }
! }
! else if (store_result)
! {
! /* must be second loop */
! if (ntuples > 0)
! {
! psql_error("too many rows\n");
! flush_error = fflush(pset.queryFout);
! PQclear(results);
! OK = false;
! break;
! }
! }
! else
! printQuery(results, &my_popt, pset.queryFout, pset.logfile);
PQclear(results);
***************
*** 1658,1660 **** expand_tilde(char **filename)
--- 1803,1855 ----
return *filename;
}
+
+
+ /*
+ * Add name of internal variable to query target list
+ *
+ */
+ TargetList
+ tglist_add(TargetList tglist, const char *name)
+ {
+ TargetListData *tgf;
+
+ tgf = pg_malloc(sizeof(TargetListData));
+ tgf->name = name ? pg_strdup(name) : NULL;
+ tgf->next = NULL;
+
+ if (tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter->next)
+ iter = iter->next;
+
+ iter->next = tgf;
+
+ return tglist;
+ }
+ else
+ return (TargetList) tgf;
+ }
+
+ /*
+ * Release target list
+ *
+ */
+ void
+ tglist_free(TargetList tglist)
+ {
+ TargetListData *iter = (TargetListData *) tglist;
+
+ while (iter)
+ {
+ TargetListData *next = iter->next;
+
+ if (iter->name)
+ free(iter->name);
+
+ free(iter);
+ iter = next;
+ }
+ }
*** a/src/bin/psql/common.h
--- b/src/bin/psql/common.h
***************
*** 14,19 ****
--- 14,27 ----
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
+ typedef struct _target_field
+ {
+ char *name;
+ struct _target_field *next;
+ } TargetListData;
+
+ typedef struct TargetListData *TargetList;
+
/*
* Safer versions of some standard C library functions. If an
* out-of-memory condition occurs, these functions will bail out
***************
*** 55,58 **** extern const char *session_username(void);
--- 63,69 ----
extern char *expand_tilde(char **filename);
+ extern TargetList tglist_add(TargetList tglist, const char *name);
+ extern void tglist_free(TargetList tglist);
+
#endif /* COMMON_H */
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 162,174 **** slashUsage(unsigned short int pager)
{
FILE *output;
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 162,175 ----
{
FILE *output;
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset NAME [, NAME [..]] execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
***************
*** 327,332 **** MainLoop(FILE *source)
--- 327,340 ----
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
+ else if (slashCmdStatus == PSQL_CMD_NOSEND)
+ {
+ resetPQExpBuffer(query_buf);
+ /* reset parsing state since we are rescanning whole line */
+ psql_scan_reset(scan_state);
+ line_saved_in_history = true;
+ success = false;
+ }
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* rescan query_buf as new input */
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 8,14 ****
#ifndef SETTINGS_H
#define SETTINGS_H
!
#include "variables.h"
#include "print.h"
--- 8,14 ----
#ifndef SETTINGS_H
#define SETTINGS_H
! #include "common.h"
#include "variables.h"
#include "print.h"
***************
*** 73,78 **** typedef struct _psqlSettings
--- 73,79 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ TargetList gvars; /* one-shot target list argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,68 ----
+ -- \gset
+ select 10, 20, 'Hello';
+ ?column? | ?column? | ?column?
+ ----------+----------+----------
+ 10 | 20 | Hello
+ (1 row)
+
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello
+ select 10, 20, 'Hello World'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ 10 20 Hello World
+ -- errors
+ \gset ,,
+ \gset ,
+ too few target variables
+ \gset ,,,
+ too many target variables
+ \gset
+ \gset: missing required argument
+ -- using badly varnames - detected when variable is accessed
+ -- note: row comments are not supported by psql lexer when command option
+ -- is read - so these statements finish by syntax error - not expected
+ -- "missing required argument" error.
+ \set --varname 10
+ \set: error while setting variable
+ \gset --varname1, varname2, varname3
+ invalid variable name: "--varname1"
+ \gset gset_test04,,
+ \echo :gset_test04
+ 10
+ \gset ,,gset_test05
+ \echo :gset_test05
+ Hello World
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+ 20
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+ select 'a', 'b', 'c'
+ \gset gset_test01, gset_test02, gset_test03
+ \echo :gset_test01 :gset_test02 :gset_test03
+ a b c
+ select 1,2 \gset x,y \\ \echo :x
+ 1
+ select 1,2 \gset x,y \echo :x \echo :y
+ 1
+ 2
+ select 1,2 \gset x,y \\ \g \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ select 1,2 \g \gset x,y \echo :x :y
+ ?column? | ?column?
+ ----------+----------
+ 1 | 2
+ (1 row)
+
+ 1 2
+ \pset null '(null)'
+ select NULL \gset var1
+ \echo :var1
+ (null)
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 109,111 **** test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
--- 109,113 ----
# run stats by itself because its delay may be insufficient under heavy load
test: stats
+
+ test: psql_cmd
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 135,137 **** test: largeobject
--- 135,138 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,57 ----
+ -- \gset
+
+ select 10, 20, 'Hello';
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 10, 20, 'Hello World'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ -- errors
+ \gset ,,
+ \gset ,
+ \gset ,,,
+ \gset
+
+ -- using badly varnames - detected when variable is accessed
+ -- note: row comments are not supported by psql lexer when command option
+ -- is read - so these statements finish by syntax error - not expected
+ -- "missing required argument" error.
+ \set --varname 10
+ \gset --varname1, varname2, varname3
+
+
+ \gset gset_test04,,
+ \echo :gset_test04
+
+ \gset ,,gset_test05
+ \echo :gset_test05
+
+ \gset ,gset_test06 ,
+ \echo :gset_test06
+
+ -- should to work with cursor too
+ \set FETCH_COUNT 1
+
+ select 'a', 'b', 'c'
+
+ \gset gset_test01, gset_test02, gset_test03
+
+ \echo :gset_test01 :gset_test02 :gset_test03
+
+ select 1,2 \gset x,y \\ \echo :x
+
+ select 1,2 \gset x,y \echo :x \echo :y
+
+ select 1,2 \gset x,y \\ \g \echo :x :y
+
+ select 1,2 \g \gset x,y \echo :x :y
+
+ \pset null '(null)'
+ select NULL \gset var1
+ \echo :var1
On Tue, Dec 18, 2012 at 2:52 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Attached updated patch
Seems fine. I'll mark this as "ready for committer".
Nice work!
--
Shigeru HANADA
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2012/12/19 Shigeru Hanada <shigeru.hanada@gmail.com>:
On Tue, Dec 18, 2012 at 2:52 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
Attached updated patch
Seems fine. I'll mark this as "ready for committer".
Nice work!
thank you very much
Regards
Pavel
--
Shigeru HANADA
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_13.diff ]
I looked at this a bit. I think you need to reconsider when and how the
\gset state gets cleaned up. Doing it inside StoreQueryResult is not
very good because that only gets reached upon successful execution.
Consider for example
select 1/0 \gset x
You'll get an ERROR from this, and a reasonable user would suppose that
that was that and the \gset is no longer in effect. But guess what,
it's still lurking under the surface, waiting to capture his next command.
This is also causing you unnecessary complication in
ExecQueryUsingCursor, which has to work around the fact that
StoreQueryResult destroys state.
I think it would be better to remove that responsibility from
StoreQueryResult and instead put the gset-list cleanup somewhere at the
end of query processing. Didn't really look into where would be the
best place, but it should be someplace that control passes through no
matter what came back from the server.
BTW, is StoreQueryResult in itself (that is, the switch on
PQresultStatus) actually doing anything useful? It appears to me that
the error cases are handled elsewhere, such that control never gets to
it unless the PQresultStatus is an expected value. If that were not the
case, printouts as laconic as "bad response\n" would certainly not be
acceptable --- people would want to see the underlying error message.
Also, I'm not sure I like the PSQL_CMD_NOSEND business, ie, trashing
the query buffer if anything can be found wrong with the \gset itself.
If I've done
big long multiline query here
\gset x y
I'd expect that the error only discards the \gset and not the query.
An error in some other sort of backslash command in that situation
wouldn't discard the query buffer. For instance try this:
regression=# select 3+2
regression-# \goofus
Invalid command \goofus. Try \? for help.
regression-# ;
?column?
----------
5
(1 row)
regression=#
So AFAICS, PSQL_CMD_NOSEND just represents extra code that's making
things worse not better.
One more gripe is that the parsing logic for \gset is pretty
unintelligible. You've got a "state" variable with only two values,
whose names seem to boil down to whether a comma is expected or not.
And then you've got a separate "process_comma" flag, which means
... well, I'm not sure, but possibly the very same thing. For sure it's
not clear whether all four possible combinations of those two variables
are valid and what they'd mean. This could use another round of
thinking and rewriting. Or at least better comments.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_13.diff ]
One more gripe is that the parsing logic for \gset is pretty
unintelligible.
After further thought, it seems to me that the problem here is an
overcomplicated definition of the \gset command; it could be made
both more usable and simpler to implement, if we looked at it
differently.
First off, why is there a provision to omit variable names for some
columns, ie why bother with saying that you can write \gset x,,y to
store only the first and third columns? If you didn't want the second
value, why didn't you leave it out of the SELECT to start with?
It seems like the only possible reason for that is if you were lazy
and typed "SELECT *" instead of listing the columns ... but then you
still need to list the columns in \gset, and it's pretty error-prone
to make sure that the \gset variable list lines up with what "*" will
emit.
In fact, it's pretty error-prone to have to make the \gset variable list
line up with the SELECT columns in any case. So here's my proposal:
let's forget the variable list entirely, and use the column names
returned by the server as the variable names to assign to. So instead
of
select 1, 2 \gset x,y
you would write
select 1 as x, 2 as y \gset
or just
select 1 x, 2 y \gset
which is exactly as much typing as the existing definition, but much
harder to screw up by misaligning the SELECT's values with the target
names. It also makes it really trivial to do the "SELECT *" case ---
you just do it, and ignore any variables for columns you don't care
about.
A probably-useful extension to this basic concept is to allow \gset
to specify an optional prefix, that is
select 1 as x, 2 as y \gset p_
would set p_x and p_y. This would make it easier to manage results from
multiple \gset operations, and to be sure that you didn't accidentally
overwrite some built-in variable.
So this seems to me to be easier and less error-prone to use than the
existing definition. It would also take a lot less code to implement,
since the parsing logic for \gset would reduce to a couple of lines,
and you'd not need the variable-name-list data structure at all.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/26/2013 11:42 AM, Tom Lane wrote:
A probably-useful extension to this basic concept is to allow \gset
to specify an optional prefix, that is
select 1 as x, 2 as y \gset p_
would set p_x and p_y. This would make it easier to manage results from
multiple \gset operations, and to be sure that you didn't accidentally
overwrite some built-in variable.
+1. This looks quite nifty. Maybe useful too to have a default prefix
via some setting.
cheers
andrew
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Andrew Dunstan <andrew@dunslane.net> writes:
+1. This looks quite nifty. Maybe useful too to have a default prefix
via some setting.
Meh. I would expect that "\gset :foo" would work to specify a computed
prefix if you wanted it --- isn't that sufficient indirection? I'm not
thrilled with further expanding the set of magic variables in psql.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
2013/1/26 Tom Lane <tgl@sss.pgh.pa.us>:
I wrote:
Pavel Stehule <pavel.stehule@gmail.com> writes:
[ gset_13.diff ]
One more gripe is that the parsing logic for \gset is pretty
unintelligible.After further thought, it seems to me that the problem here is an
overcomplicated definition of the \gset command; it could be made
both more usable and simpler to implement, if we looked at it
differently.First off, why is there a provision to omit variable names for some
columns, ie why bother with saying that you can write \gset x,,y to
store only the first and third columns? If you didn't want the second
value, why didn't you leave it out of the SELECT to start with?
It seems like the only possible reason for that is if you were lazy
and typed "SELECT *" instead of listing the columns ... but then you
still need to list the columns in \gset, and it's pretty error-prone
to make sure that the \gset variable list lines up with what "*" will
emit.
possibility to skip some variables is David Fetter's idea. I see only
one possible use case - it enable use some query from history without
necessity to modify query or creating some auxiliary variables.
Personally, I can live without this feature, but it question for David
mainly.
In fact, it's pretty error-prone to have to make the \gset variable list
line up with the SELECT columns in any case. So here's my proposal:
let's forget the variable list entirely, and use the column names
returned by the server as the variable names to assign to. So instead
of
select 1, 2 \gset x,y
you would write
select 1 as x, 2 as y \gset
or just
select 1 x, 2 y \gset
which is exactly as much typing as the existing definition, but much
harder to screw up by misaligning the SELECT's values with the target
names. It also makes it really trivial to do the "SELECT *" case ---
you just do it, and ignore any variables for columns you don't care
about.
hard to say
your proposal has advantages - and implementation is simple, but it is
looking little bit strange - but like other psql features.
I have no strong opinion - I prefer original design, as more explicit
with clean separation line between query and console part, but I am
able to see advantages of your proposal - so depends what will speak
others. I have no problem with your design, although I am thinking so
original design is little bit more safer (but not with significant
difference).
A probably-useful extension to this basic concept is to allow \gset
to specify an optional prefix, that is
select 1 as x, 2 as y \gset p_
would set p_x and p_y. This would make it easier to manage results from
multiple \gset operations, and to be sure that you didn't accidentally
overwrite some built-in variable.
I understand to motivation - but I am not enthused. Now - a work with
variables is strange - and with it will be more stranger.
So this seems to me to be easier and less error-prone to use than the
existing definition. It would also take a lot less code to implement,
since the parsing logic for \gset would reduce to a couple of lines,
and you'd not need the variable-name-list data structure at all.
I will waiting for others - I can live with this proposal.
Regards
Pavel
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
2013/1/26 Tom Lane <tgl@sss.pgh.pa.us>:
Andrew Dunstan <andrew@dunslane.net> writes:
+1. This looks quite nifty. Maybe useful too to have a default prefix
via some setting.Meh. I would expect that "\gset :foo" would work to specify a computed
prefix if you wanted it --- isn't that sufficient indirection? I'm not
thrilled with further expanding the set of magic variables in psql.
here is patch related to your proposal
Regards
Pavel
Show quoted text
regards, tom lane
Attachments:
gset_20130128.patchapplication/octet-stream; name=gset_20130128.patchDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1648 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">prefix</replaceable></term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row. You can define <replaceable
+ class="parameter">prefix</replaceable>, that will be used as prefix
+ for target variables.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello' AS var1, 'wonderful' AS var2, 'world!' AS var3 \gset result_
+ foo=> \echo :result_var1 :result_var3
+ hello world!
+ </programlisting>
+ </para>
+ <para>
+ When this command fails, then related <replaceable
+ class="parameter">variables</replaceable> has undefined content.
+ </para>
+ </listitem>
+ </varlistentry>
+
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,767 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send a query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ char *prefix = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false);
+
+ if (!prefix)
+ pset.gvprefix = pg_strdup("");
+ else
+ pset.gvprefix = prefix;
+
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 833,838 **** PrintQueryResults(PGresult *results)
--- 833,947 ----
/*
+ * StoreQueryTuple: assuming query result is OK, save first tuple
+ *
+ * Returns true if successful, false otherwise.
+ */
+ static bool
+ StoreQueryTuple(PGresult *result)
+ {
+ bool success = true;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ int i;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ char *value;
+ char *varname;
+ char *colname = PQfname(result, i);
+
+ if (PQgetisnull(result, 0, i))
+ value = pset.popt.nullPrint ? pset.popt.nullPrint : "";
+ else
+ value = PQgetvalue(result, 0, i);
+
+ if (strcmp(pset.gvprefix, "") != 0)
+ {
+ /* concate prefix and column name */
+ varname = pg_malloc(strlen(pset.gvprefix) + strlen(colname) + 1);
+ strcpy(varname, pset.gvprefix);
+ strcat(varname, colname);
+ }
+ else
+ varname = pg_strdup(colname);
+
+ if (!SetVariable(pset.vars, varname, value))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ varname);
+ success = false;
+ break;
+ }
+
+ free(varname);
+ }
+ }
+
+ return success;
+ }
+
+
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ success = StoreQueryTuple(result);
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ return success;
+ }
+
+
+ /*
* SendQuery: send the query string to the backend
* (and print out results)
*
***************
*** 958,964 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1067,1078 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvprefix)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1034,1039 **** SendQuery(const char *query)
--- 1148,1160 ----
PQclear(results);
+ /* clean gset prefix if was used */
+ if (pset.gvprefix)
+ {
+ free(pset.gvprefix);
+ pset.gvprefix = NULL;
+ }
+
/* Possible microtiming output */
if (pset.timing)
printf(_("Time: %.3f ms\n"), elapsed_msec);
***************
*** 1082,1087 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1203,1209 ----
instr_time before,
after;
int flush_error;
+ bool first_iteration = true;
*elapsed_msec = 0;
***************
*** 1182,1187 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1304,1336 ----
ntuples = PQntuples(results);
+ if (pset.gvprefix)
+ {
+ /* second iteration should to return zero rows */
+ if (first_iteration)
+ {
+ OK = StoreQueryResult(results);
+ PQclear(results);
+ first_iteration = false;
+ }
+ else if (ntuples > 0)
+ {
+ psql_error("too many rows\n");
+ OK = false;
+ }
+
+ if (!OK)
+ {
+ flush_error = fflush(pset.queryFout);
+ break;
+ }
+
+ if (ntuples < pset.fetch_count || cancel_pressed)
+ break;
+ else
+ continue;
+ }
+
if (ntuples < pset.fetch_count)
{
/* this is the last result set, so allow footer decoration */
***************
*** 1237,1242 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1386,1396 ----
free(pset.gfname);
pset.gfname = NULL;
}
+ else if (pset.gvprefix)
+ {
+ free(pset.gvprefix);
+ pset.gvprefix = NULL;
+ }
else if (did_pager)
{
ClosePager(pset.queryFout);
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 165,177 **** slashUsage(unsigned short int pager)
currdb = PQdb(pset.db);
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 165,178 ----
currdb = PQdb(pset.db);
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset prefix execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 73,78 **** typedef struct _psqlSettings
--- 73,79 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ char *gvprefix; /* one-shot prefix argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,62 ----
+ -- \gset
+ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
+ \echo :pref01_test01 :pref01_test02 :pref01_test03
+ 10 20 Hello
+ -- should fail bad name
+ select 10 as "bad name" \gset
+ invalid variable name: "bad name"
+ -- more in one line
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ 1
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ 3
+ 4
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 5 | 6
+ (1 row)
+
+ 5 6
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 7 | 8
+ (1 row)
+
+ 7 8
+ \pset null '(null)'
+ select NULL as var1 \gset pref01_
+ \echo :pref01_var1
+ (null)
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ too many rows
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+ no data found
+ -- should to work with cursors
+ \set FETCH_COUNT 1
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ 1
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ 3
+ 4
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 5 | 6
+ (1 row)
+
+ 5 6
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 7 | 8
+ (1 row)
+
+ 7 8
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ too many rows
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+ no data found
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 21,26 **** test: strings
--- 21,31 ----
test: numerology
# ----------
+ # verify console features
+ # ----------
+ test: psql_cmd
+
+ # ----------
# The second group of parallel tests
# ----------
test: point lseg box path polygon circle date time timetz timestamp timestamptz interval abstime reltime tinterval inet macaddr tstypes comments
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 135,137 **** test: largeobject
--- 135,138 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,33 ----
+ -- \gset
+
+ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
+
+ \echo :pref01_test01 :pref01_test02 :pref01_test03
+
+ -- should fail bad name
+ select 10 as "bad name" \gset
+
+ -- more in one line
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+
+ \pset null '(null)'
+ select NULL as var1 \gset pref01_
+ \echo :pref01_var1
+
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+
+ -- should to work with cursors
+ \set FETCH_COUNT 1
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
Hello
can you look, please, on updated version - it respects Tom's proposal
and it is significantly reduced?
Thank you
Pavel Stehule
2013/1/28 Pavel Stehule <pavel.stehule@gmail.com>:
Hello
2013/1/26 Tom Lane <tgl@sss.pgh.pa.us>:
Andrew Dunstan <andrew@dunslane.net> writes:
+1. This looks quite nifty. Maybe useful too to have a default prefix
via some setting.Meh. I would expect that "\gset :foo" would work to specify a computed
prefix if you wanted it --- isn't that sufficient indirection? I'm not
thrilled with further expanding the set of magic variables in psql.here is patch related to your proposal
Regards
Pavel
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Pavel Stehule <pavel.stehule@gmail.com> writes:
here is patch related to your proposal
I looked at this a bit. It's getting there, though I still don't trust
the places where you've chosen to clear the prefix setting. (Looking at
it, I'm now not sure that I trust the implementation of \g either.)
However, what I wanted to ask about was this:
+ if (PQgetisnull(result, 0, i)) + value = pset.popt.nullPrint ? pset.popt.nullPrint : ""; + else + value = PQgetvalue(result, 0, i);
What's the argument for using nullPrint here? ISTM that's effectively a
form of escaping, and I'd not expect that to get applied to values going
into variables, any more than any other formatting we do when printing
results.
Admittedly, if we just take the PQgetvalue result blindly, there'll
be no way to tell the difference between empty-string and NULL results.
But I'm not convinced that this approach is better. It would certainly
need more than no documentation.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
2013/2/1 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
here is patch related to your proposal
I looked at this a bit. It's getting there, though I still don't trust
the places where you've chosen to clear the prefix setting. (Looking at
it, I'm now not sure that I trust the implementation of \g either.)However, what I wanted to ask about was this:
+ if (PQgetisnull(result, 0, i)) + value = pset.popt.nullPrint ? pset.popt.nullPrint : ""; + else + value = PQgetvalue(result, 0, i);What's the argument for using nullPrint here? ISTM that's effectively a
form of escaping, and I'd not expect that to get applied to values going
into variables, any more than any other formatting we do when printing
results.Admittedly, if we just take the PQgetvalue result blindly, there'll
be no way to tell the difference between empty-string and NULL results.
But I'm not convinced that this approach is better. It would certainly
need more than no documentation.
I have not strong opinion about storing NULL value - and nullPrint is
a best from simple variants -
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String
@d is subset of @c, and I think so it put some better possibilities
with only two lines more
@a is probably best - but significant change - not hard to implement it
SELECT NULL AS x \g pref_
SELECT :'pref_' IS NULL;
is can be nice
but it should be premature optimization too - nullPrint is enough for
typical use cases
Regards
Pavel
Regards
Pavel
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Feb 2, 2013 at 7:30 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String
+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.
--
Shigeru HANADA
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/2/2 Shigeru Hanada <shigeru.hanada@gmail.com>:
On Sat, Feb 2, 2013 at 7:30 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.
yes, this has one other advantage - it doesn't block possible
enhancing variables about NULL support in future. And other - it
doesn't depends on psql settings
Regards
Pavel
--
Shigeru HANADA
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
On Sat, Feb 2, 2013 at 7:30 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String
+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.
Yeah, I was considering that one too. Let's do it that way.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello
2013/2/2 Tom Lane <tgl@sss.pgh.pa.us>:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
On Sat, Feb 2, 2013 at 7:30 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.Yeah, I was considering that one too. Let's do it that way.
updated version
Regards
Pavel
Show quoted text
regards, tom lane
Attachments:
gset_20130202.patchapplication/octet-stream; name=gset_20130202.patchDownload
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
***************
*** 1617,1622 **** Tue Oct 26 21:40:57 CEST 1999
--- 1617,1654 ----
</varlistentry>
<varlistentry>
+ <term><literal>\gset</literal> <replaceable class="parameter">prefix</replaceable></term>
+
+ <listitem>
+ <para>
+ Sends the current query input buffer to the server and stores the
+ query's output into corresponding <replaceable
+ class="parameter">variable</replaceable>. The preceding query must
+ return only one row. You can define <replaceable
+ class="parameter">prefix</replaceable>, that will be used as prefix
+ for target variables.
+ Example:
+ <programlisting>
+ foo=> SELECT 'hello' AS var1, 'wonderful' AS var2, 'world!' AS var3 \gset result_
+ foo=> \echo :result_var1 :result_var3
+ hello world!
+ </programlisting>
+ </para>
+ <para>
+ When this command fails, then related <replaceable
+ class="parameter">variables</replaceable> has undefined content.
+ </para>
+ <para>
+ Assign <literal>NULL</literal> to target <replaceable
+ class="parameter">variable</replaceable> is same as cleaning this
+ <replaceable class="parameter">variable</replaceable>. Possible to use
+ coalesce as protection against to this behave.
+ </para>
+ </listitem>
+ </varlistentry>
+
+
+ <varlistentry>
<term><literal>\h</literal> or <literal>\help</literal> <literal>[ <replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 748,753 **** exec_command(const char *cmd,
--- 748,767 ----
status = PSQL_CMD_SEND;
}
+ /* \gset send a query and store result */
+ else if (strcmp(cmd, "gset") == 0)
+ {
+ char *prefix = psql_scan_slash_option(scan_state,
+ OT_NORMAL, NULL, false);
+
+ if (!prefix)
+ pset.gvprefix = pg_strdup("");
+ else
+ pset.gvprefix = prefix;
+
+ status = PSQL_CMD_SEND;
+ }
+
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 818,823 **** PrintQueryResults(PGresult *results)
--- 818,950 ----
/*
+ * StoreQueryTuple: assuming query result is OK, save first tuple
+ *
+ * Returns true if successful, false otherwise.
+ */
+ static bool
+ StoreQueryTuple(PGresult *result)
+ {
+ bool success = true;
+
+ if (PQntuples(result) < 1)
+ {
+ psql_error("no data found\n");
+ success = false;
+ }
+ else if (PQntuples(result) > 1)
+ {
+ psql_error("too many rows\n");
+ success = false;
+ }
+ else
+ {
+ int i;
+
+ for (i = 0; i < PQnfields(result); i++)
+ {
+ char *varname;
+ char *colname = PQfname(result, i);
+
+ if (strcmp(pset.gvprefix, "") != 0)
+ {
+ /* concate prefix and column name */
+ varname = pg_malloc(strlen(pset.gvprefix) + strlen(colname) + 1);
+ strcpy(varname, pset.gvprefix);
+ strcat(varname, colname);
+ }
+ else
+ varname = pg_strdup(colname);
+
+ if (!PQgetisnull(result, 0, i))
+ {
+ char *value = PQgetvalue(result, 0, i);
+
+ if (!SetVariable(pset.vars, varname, value))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ varname);
+ success = false;
+ break;
+ }
+ }
+ else
+ {
+ /*
+ * we don't try to substitute NULL by some other value - current
+ * implementation of variables doesn't support NULL, so when some
+ * result is NULL, then just clean a target variable. We still
+ * request valid variable name.
+ */
+ if (!valid_variable_name(varname))
+ {
+ psql_error("invalid variable name: \"%s\"\n",
+ varname);
+ success = false;
+ break;
+ }
+
+ /* ignore result, variable should not exists */
+ DeleteVariable(pset.vars, varname);
+ }
+
+ free(varname);
+ }
+ }
+
+ return success;
+ }
+
+
+ /*
+ * StoreQueryResult: store first row of result to selected variables
+ *
+ * Note: Utility function for use by SendQuery() only.
+ *
+ * Returns true if the query executed successfully, false otherwise.
+ */
+ static bool
+ StoreQueryResult(PGresult *result)
+ {
+ bool success;
+
+ switch (PQresultStatus(result))
+ {
+ case PGRES_TUPLES_OK:
+ success = StoreQueryTuple(result);
+ break;
+
+ case PGRES_COMMAND_OK:
+ case PGRES_EMPTY_QUERY:
+ psql_error("no data found\n");
+ success = false;
+ break;
+
+ case PGRES_COPY_OUT:
+ case PGRES_COPY_IN:
+ psql_error("COPY is not supported by \\gset command\n");
+ success = false;
+ break;
+
+ case PGRES_BAD_RESPONSE:
+ case PGRES_NONFATAL_ERROR:
+ case PGRES_FATAL_ERROR:
+ success = false;
+ psql_error("bad response\n");
+ break;
+
+ default:
+ success = false;
+ psql_error("unexpected PQresultStatus: %d\n",
+ PQresultStatus(result));
+ break;
+ }
+
+ return success;
+ }
+
+
+ /*
* SendQuery: send the query string to the backend
* (and print out results)
*
***************
*** 943,949 **** SendQuery(const char *query)
/* but printing results isn't: */
if (OK && results)
! OK = PrintQueryResults(results);
}
else
{
--- 1070,1081 ----
/* but printing results isn't: */
if (OK && results)
! {
! if (pset.gvprefix)
! OK = StoreQueryResult(results);
! else
! OK = PrintQueryResults(results);
! }
}
else
{
***************
*** 1019,1024 **** SendQuery(const char *query)
--- 1151,1163 ----
PQclear(results);
+ /* clean gset prefix if was used */
+ if (pset.gvprefix)
+ {
+ free(pset.gvprefix);
+ pset.gvprefix = NULL;
+ }
+
/* Possible microtiming output */
if (pset.timing)
printf(_("Time: %.3f ms\n"), elapsed_msec);
***************
*** 1067,1072 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1206,1212 ----
instr_time before,
after;
int flush_error;
+ bool first_iteration = true;
*elapsed_msec = 0;
***************
*** 1167,1172 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1307,1339 ----
ntuples = PQntuples(results);
+ if (pset.gvprefix)
+ {
+ /* second iteration should to return zero rows */
+ if (first_iteration)
+ {
+ OK = StoreQueryResult(results);
+ PQclear(results);
+ first_iteration = false;
+ }
+ else if (ntuples > 0)
+ {
+ psql_error("too many rows\n");
+ OK = false;
+ }
+
+ if (!OK)
+ {
+ flush_error = fflush(pset.queryFout);
+ break;
+ }
+
+ if (ntuples < pset.fetch_count || cancel_pressed)
+ break;
+ else
+ continue;
+ }
+
if (ntuples < pset.fetch_count)
{
/* this is the last result set, so allow footer decoration */
***************
*** 1222,1227 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec)
--- 1389,1399 ----
free(pset.gfname);
pset.gfname = NULL;
}
+ else if (pset.gvprefix)
+ {
+ free(pset.gvprefix);
+ pset.gvprefix = NULL;
+ }
else if (did_pager)
{
ClosePager(pset.queryFout);
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
***************
*** 165,177 **** slashUsage(unsigned short int pager)
currdb = PQdb(pset.db);
! output = PageOutput(94, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
--- 165,178 ----
currdb = PQdb(pset.db);
! output = PageOutput(95, pager);
/* if you add/remove a line here, change the row count above */
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
+ fprintf(output, _(" \\gset prefix execute query and store result in internal variables\n"));
fprintf(output, _(" \\h [NAME] help on syntax of SQL commands, * for all commands\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, "\n");
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
***************
*** 73,78 **** typedef struct _psqlSettings
--- 73,79 ----
printQueryOpt popt;
char *gfname; /* one-shot file output argument for \g */
+ char *gvprefix; /* one-shot prefix argument for \gset */
bool notty; /* stdin or stdout is not a tty (as determined
* on startup) */
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 856,862 **** psql_completion(char *text, int start, int end)
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
--- 856,862 ----
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",
"\\e", "\\echo", "\\ef", "\\encoding",
! "\\f", "\\g", "\\gset", "\\h", "\\help", "\\H", "\\i", "\\ir", "\\l",
"\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
"\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", "\\r",
"\\set", "\\sf", "\\t", "\\T",
*** a/src/bin/psql/variables.c
--- b/src/bin/psql/variables.c
***************
*** 18,24 ****
* underscore. Keep this in sync with the definition of variable_char in
* psqlscan.l.
*/
! static bool
valid_variable_name(const char *name)
{
const unsigned char *ptr = (const unsigned char *) name;
--- 18,24 ----
* underscore. Keep this in sync with the definition of variable_char in
* psqlscan.l.
*/
! bool
valid_variable_name(const char *name)
{
const unsigned char *ptr = (const unsigned char *) name;
*** a/src/bin/psql/variables.h
--- b/src/bin/psql/variables.h
***************
*** 48,53 **** int GetVariableNum(VariableSpace space,
--- 48,54 ----
void PrintVariables(VariableSpace space);
+ bool valid_variable_name(const char *name);
bool SetVariable(VariableSpace space, const char *name, const char *value);
bool SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook);
bool SetVariableBool(VariableSpace space, const char *name);
*** /dev/null
--- b/src/test/regress/expected/psql_cmd.out
***************
*** 0 ****
--- 1,67 ----
+ -- \gset
+ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
+ \echo :pref01_test01 :pref01_test02 :pref01_test03
+ 10 20 Hello
+ -- should fail bad name
+ select 10 as "bad name" \gset
+ invalid variable name: "bad name"
+ select NULL as "bad name" \gset
+ invalid variable name: "bad name"
+ -- more in one line
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ 1
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ 3
+ 4
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 5 | 6
+ (1 row)
+
+ 5 6
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 7 | 8
+ (1 row)
+
+ 7 8
+ -- NULL handling
+ select 'Hello' as var1 \gset pref01_
+ \echo :pref01_var1
+ Hello
+ select NULL as var1 \gset pref01_
+ \echo :pref01_var1
+ :pref01_var1
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ too many rows
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+ no data found
+ -- should to work with cursors
+ \set FETCH_COUNT 1
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ 1
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ 3
+ 4
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 5 | 6
+ (1 row)
+
+ 5 6
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+ x | y
+ ---+---
+ 7 | 8
+ (1 row)
+
+ 7 8
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ too many rows
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+ no data found
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 21,26 **** test: strings
--- 21,31 ----
test: numerology
# ----------
+ # verify console features
+ # ----------
+ test: psql_cmd
+
+ # ----------
# The second group of parallel tests
# ----------
test: point lseg box path polygon circle date time timetz timestamp timestamptz interval abstime reltime tinterval inet macaddr tstypes comments
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
***************
*** 135,137 **** test: largeobject
--- 135,138 ----
test: with
test: xml
test: stats
+ test: psql_cmd
*** /dev/null
--- b/src/test/regress/sql/psql_cmd.sql
***************
*** 0 ****
--- 1,36 ----
+ -- \gset
+
+ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
+
+ \echo :pref01_test01 :pref01_test02 :pref01_test03
+
+ -- should fail bad name
+ select 10 as "bad name" \gset
+ select NULL as "bad name" \gset
+
+ -- more in one line
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+
+ -- NULL handling
+ select 'Hello' as var1 \gset pref01_
+ \echo :pref01_var1
+ select NULL as var1 \gset pref01_
+ \echo :pref01_var1
+
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
+
+ -- should to work with cursors
+ \set FETCH_COUNT 1
+ select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
+ select 3 as x, 4 as y \gset pref01_ \echo :pref01_x \echo :pref01_y
+ select 5 as x, 6 as y \gset pref01_ \\ \g \echo :pref01_x :pref01_y
+ select 7 as x, 8 as y \g \gset pref01_ \echo :pref01_x :pref01_y
+
+ --should file - no returned one tuple
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) \gset pref01_
+ select 10 as test01, 20 as test02, 'Hello' as test03 from generate_series(1,3) where false \gset pref01_
Pavel Stehule <pavel.stehule@gmail.com> writes:
2013/2/2 Tom Lane <tgl@sss.pgh.pa.us>:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.
Yeah, I was considering that one too. Let's do it that way.
updated version
Applied with corrections.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/2/2 Tom Lane <tgl@sss.pgh.pa.us>:
Shigeru Hanada <shigeru.hanada@gmail.com> writes:
On Sat, Feb 2, 2013 at 7:30 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
possible variants
a) don't store NULL values - and remove existing variable when NULL
be assigned - it is probably best, but should be confusing for users
b) implement flag IS NULL - for variables
c) use nullPrint
d) use empty String+1 for a). If users want to determine whether the result was NULL, or
want to use substitute string for NULL result, they can use coalesce
in SELECT clause. Anyway the feature should be documented clearly.Yeah, I was considering that one too. Let's do it that way.
regards, tom lane
possible simple enhancing of this behave (for 9.4).
now missing variables is replaced by variable's name. We can implement
some pset option - some like define what do with missing variable
\pset missing_variable (use_name | use_null | error )
when this option will be active, then missing variable will be
replaced by NULL. With this feature sequences of SQL statements joined
by some variables can work.
SELECT NULL as myvar \gset
\pset missing_variable use_null
SELECT :'myvar' IS NULL;
ideas, opinions ?
Regards
Pavel
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Pavel Stehule <pavel.stehule@gmail.com> writes:
now missing variables is replaced by variable's name. We can implement
some pset option - some like define what do with missing variable
\pset missing_variable (use_name | use_null | error )
No, it isn't "replaced by variable's name". What actually happens is we
don't attempt a replacement unless the string after the colon matches an
existing variable. Tampering with that seems dangerous and foolish.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/2/3 Tom Lane <tgl@sss.pgh.pa.us>:
Pavel Stehule <pavel.stehule@gmail.com> writes:
now missing variables is replaced by variable's name. We can implement
some pset option - some like define what do with missing variable\pset missing_variable (use_name | use_null | error )
No, it isn't "replaced by variable's name". What actually happens is we
don't attempt a replacement unless the string after the colon matches an
existing variable. Tampering with that seems dangerous and foolish.
some other ideas?
do you think so full NULL support has sense?
Regards
Pavel
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers