Add \gdesc psql command.

This command acts somewhat like \g, but instead of executing the query
buffer, it merely prints a description of the columns that the query
result would have.  (Of course, this still requires parsing the query;
if parse analysis fails, you get an error anyway.)  We accomplish this
using an unnamed prepared statement, which should be invisible to psql
users.

Pavel Stehule, reviewed by Fabien Coelho

Discussion: https://postgr.es/m/CAFj8pRBhYVvO34FU=EKb=nAF5t3b++krKt1FneCmR0kuF5m-QA@mail.gmail.com
This commit is contained in:
Tom Lane 2017-09-05 18:17:47 -04:00
parent 6e427aa4e5
commit 49ca462eb1
8 changed files with 293 additions and 6 deletions

View File

@ -1949,6 +1949,25 @@ Tue Oct 26 21:40:57 CEST 1999
</varlistentry>
<varlistentry>
<term><literal>\gdesc</literal></term>
<listitem>
<para>
Shows the description (that is, the column names and data types)
of the result of the current query buffer. The query is not
actually executed; however, if it contains some type of syntax
error, that error will be reported in the normal way.
</para>
<para>
If the current query buffer is empty, the most recently sent query
is described instead.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>\gexec</literal></term>

View File

@ -88,6 +88,7 @@ static backslashResult exec_command_errverbose(PsqlScanState scan_state, bool ac
static backslashResult exec_command_f(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_g(PsqlScanState scan_state, bool active_branch,
const char *cmd);
static backslashResult exec_command_gdesc(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_gset(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_help(PsqlScanState scan_state, bool active_branch);
@ -337,6 +338,8 @@ exec_command(const char *cmd,
status = exec_command_f(scan_state, active_branch);
else if (strcmp(cmd, "g") == 0 || strcmp(cmd, "gx") == 0)
status = exec_command_g(scan_state, active_branch, cmd);
else if (strcmp(cmd, "gdesc") == 0)
status = exec_command_gdesc(scan_state, active_branch);
else if (strcmp(cmd, "gexec") == 0)
status = exec_command_gexec(scan_state, active_branch);
else if (strcmp(cmd, "gset") == 0)
@ -1330,6 +1333,23 @@ exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd)
return status;
}
/*
* \gdesc -- describe query result
*/
static backslashResult
exec_command_gdesc(PsqlScanState scan_state, bool active_branch)
{
backslashResult status = PSQL_CMD_SKIP_LINE;
if (active_branch)
{
pset.gdesc_flag = true;
status = PSQL_CMD_SEND;
}
return status;
}
/*
* \gexec -- send query and execute each field of result
*/

View File

@ -29,6 +29,7 @@
#include "fe_utils/mbprint.h"
static bool DescribeQuery(const char *query, double *elapsed_msec);
static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
static bool command_no_begin(const char *query);
static bool is_select_command(const char *query);
@ -1323,8 +1324,15 @@ SendQuery(const char *query)
}
}
if (pset.fetch_count <= 0 || pset.gexec_flag ||
pset.crosstab_flag || !is_select_command(query))
if (pset.gdesc_flag)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
ResetCancelConn();
results = NULL; /* PQclear(NULL) does nothing */
}
else if (pset.fetch_count <= 0 || pset.gexec_flag ||
pset.crosstab_flag || !is_select_command(query))
{
/* Default fetch-it-all-and-print mode */
instr_time before,
@ -1467,6 +1475,9 @@ sendquery_cleanup:
pset.gset_prefix = NULL;
}
/* reset \gdesc trigger */
pset.gdesc_flag = false;
/* reset \gexec trigger */
pset.gexec_flag = false;
@ -1482,6 +1493,118 @@ sendquery_cleanup:
}
/*
* DescribeQuery: describe the result columns of a query, without executing it
*
* Returns true if the operation executed successfully, false otherwise.
*
* If pset.timing is on, total query time (exclusive of result-printing) is
* stored into *elapsed_msec.
*/
static bool
DescribeQuery(const char *query, double *elapsed_msec)
{
PGresult *results;
bool OK;
instr_time before,
after;
*elapsed_msec = 0;
if (pset.timing)
INSTR_TIME_SET_CURRENT(before);
/*
* To parse the query but not execute it, we prepare it, using the unnamed
* prepared statement. This is invisible to psql users, since there's no
* way to access the unnamed prepared statement from psql user space. The
* next Parse or Query protocol message would overwrite the statement
* anyway. (So there's no great need to clear it when done, which is a
* good thing because libpq provides no easy way to do that.)
*/
results = PQprepare(pset.db, "", query, 0, NULL);
if (PQresultStatus(results) != PGRES_COMMAND_OK)
{
psql_error("%s", PQerrorMessage(pset.db));
ClearOrSaveResult(results);
return false;
}
PQclear(results);
results = PQdescribePrepared(pset.db, "");
OK = AcceptResult(results) &&
(PQresultStatus(results) == PGRES_COMMAND_OK);
if (OK && results)
{
if (PQnfields(results) > 0)
{
PQExpBufferData buf;
int i;
initPQExpBuffer(&buf);
printfPQExpBuffer(&buf,
"SELECT name AS \"%s\", pg_catalog.format_type(tp, tpm) AS \"%s\"\n"
"FROM (VALUES ",
gettext_noop("Column"),
gettext_noop("Type"));
for (i = 0; i < PQnfields(results); i++)
{
const char *name;
char *escname;
if (i > 0)
appendPQExpBufferStr(&buf, ",");
name = PQfname(results, i);
escname = PQescapeLiteral(pset.db, name, strlen(name));
if (escname == NULL)
{
psql_error("%s", PQerrorMessage(pset.db));
PQclear(results);
termPQExpBuffer(&buf);
return false;
}
appendPQExpBuffer(&buf, "(%s, '%u'::pg_catalog.oid, %d)",
escname,
PQftype(results, i),
PQfmod(results, i));
PQfreemem(escname);
}
appendPQExpBufferStr(&buf, ") s(name, tp, tpm)");
PQclear(results);
results = PQexec(pset.db, buf.data);
OK = AcceptResult(results);
if (pset.timing)
{
INSTR_TIME_SET_CURRENT(after);
INSTR_TIME_SUBTRACT(after, before);
*elapsed_msec += INSTR_TIME_GET_MILLISEC(after);
}
if (OK && results)
OK = PrintQueryResults(results);
termPQExpBuffer(&buf);
}
else
fprintf(pset.queryFout,
_("The command has no result, or the result has no columns.\n"));
}
ClearOrSaveResult(results);
return OK;
}
/*
* ExecQueryUsingCursor: run a SELECT-like query using a cursor
*
@ -1627,7 +1750,9 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
break;
}
/* Note we do not deal with \gexec or \crosstabview modes here */
/*
* Note we do not deal with \gdesc, \gexec or \crosstabview modes here
*/
ntuples = PQntuples(results);

View File

@ -167,13 +167,14 @@ slashUsage(unsigned short int pager)
* Use "psql --help=commands | wc" to count correctly. It's okay to count
* the USE_READLINE line even in builds without that.
*/
output = PageOutput(122, pager ? &(pset.popt.topt) : NULL);
output = PageOutput(125, pager ? &(pset.popt.topt) : NULL);
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
fprintf(output, _(" \\errverbose show most recent error message at maximum verbosity\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\gdesc describe result of query, without executing it\n"));
fprintf(output, _(" \\gexec execute query, then execute each value in its result\n"));
fprintf(output, _(" \\gset [PREFIX] execute query and store results in psql variables\n"));
fprintf(output, _(" \\gx [FILE] as \\g, but forces expanded output mode\n"));

View File

@ -93,7 +93,8 @@ typedef struct _psqlSettings
char *gfname; /* one-shot file output argument for \g */
bool g_expanded; /* one-shot expanded output requested via \gx */
char *gset_prefix; /* one-shot prefix argument for \gset */
bool gexec_flag; /* one-shot flag to execute query's results */
bool gdesc_flag; /* one-shot request to describe query results */
bool gexec_flag; /* one-shot request to execute query results */
bool crosstab_flag; /* one-shot request to crosstab results */
char *ctv_args[4]; /* \crosstabview arguments */

View File

@ -1433,7 +1433,7 @@ psql_completion(const char *text, int start, int end)
"\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding",
"\\endif", "\\errverbose", "\\ev",
"\\f",
"\\g", "\\gexec", "\\gset", "\\gx",
"\\g", "\\gdesc", "\\gexec", "\\gset", "\\gx",
"\\h", "\\help", "\\H",
"\\i", "\\if", "\\ir",
"\\l", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",

View File

@ -126,6 +126,91 @@ more than one row returned for \gset
select 10 as test01, 20 as test02 from generate_series(1,0) \gset
no rows returned for \gset
\unset FETCH_COUNT
-- \gdesc
SELECT
NULL AS zero,
1 AS one,
2.0 AS two,
'three' AS three,
$1 AS four,
sin($2) as five,
'foo'::varchar(4) as six,
CURRENT_DATE AS now
\gdesc
Column | Type
--------+----------------------
zero | text
one | integer
two | numeric
three | text
four | text
five | double precision
six | character varying(4)
now | date
(8 rows)
-- should work with tuple-returning utilities, such as EXECUTE
PREPARE test AS SELECT 1 AS first, 2 AS second;
EXECUTE test \gdesc
Column | Type
--------+---------
first | integer
second | integer
(2 rows)
EXPLAIN EXECUTE test \gdesc
Column | Type
------------+------
QUERY PLAN | text
(1 row)
-- should fail cleanly - syntax error
SELECT 1 + \gdesc
ERROR: syntax error at end of input
LINE 1: SELECT 1 +
^
-- check behavior with empty results
SELECT \gdesc
The command has no result, or the result has no columns.
CREATE TABLE bububu(a int) \gdesc
The command has no result, or the result has no columns.
-- subject command should not have executed
TABLE bububu; -- fail
ERROR: relation "bububu" does not exist
LINE 1: TABLE bububu;
^
-- query buffer should remain unchanged
SELECT 1 AS x, 'Hello', 2 AS y, true AS "dirty\name"
\gdesc
Column | Type
------------+---------
x | integer
?column? | text
y | integer
dirty\name | boolean
(4 rows)
\g
x | ?column? | y | dirty\name
---+----------+---+------------
1 | Hello | 2 | t
(1 row)
-- all on one line
SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
Column | Type
------------+---------
x | integer
?column? | text
y | integer
dirty\name | boolean
(4 rows)
x | ?column? | y | dirty\name
---+----------+---+------------
3 | Hello | 4 | t
(1 row)
-- \gexec
create temporary table gexec_test(a int, b text, c date, d float);
select format('create index on gexec_test(%I)', attname)

View File

@ -73,6 +73,42 @@ select 10 as test01, 20 as test02 from generate_series(1,0) \gset
\unset FETCH_COUNT
-- \gdesc
SELECT
NULL AS zero,
1 AS one,
2.0 AS two,
'three' AS three,
$1 AS four,
sin($2) as five,
'foo'::varchar(4) as six,
CURRENT_DATE AS now
\gdesc
-- should work with tuple-returning utilities, such as EXECUTE
PREPARE test AS SELECT 1 AS first, 2 AS second;
EXECUTE test \gdesc
EXPLAIN EXECUTE test \gdesc
-- should fail cleanly - syntax error
SELECT 1 + \gdesc
-- check behavior with empty results
SELECT \gdesc
CREATE TABLE bububu(a int) \gdesc
-- subject command should not have executed
TABLE bububu; -- fail
-- query buffer should remain unchanged
SELECT 1 AS x, 'Hello', 2 AS y, true AS "dirty\name"
\gdesc
\g
-- all on one line
SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
-- \gexec
create temporary table gexec_test(a int, b text, c date, d float);