*** 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 ---- + \gset prefix + + + + Sends the current query input buffer to the server and stores the + query's output into corresponding variable. The preceding query must + return only one row. You can define prefix, that will be used as prefix + for target variables. + Example: + + foo=> SELECT 'hello' AS var1, 'wonderful' AS var2, 'world!' AS var3 \gset result_ + foo=> \echo :result_var1 :result_var3 + hello world! + + + + When this command fails, then related variables has undefined content. + + + Assign NULL to target variable is same as cleaning this + variable. Possible to use + coalesce as protection against to this behave. + + + + + + \h or \help [ command ] *** 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_