From 49ca462eb165dea297f1f110e8eac064308e9d51 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 5 Sep 2017 18:17:47 -0400 Subject: [PATCH] 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 --- doc/src/sgml/ref/psql-ref.sgml | 19 +++++ src/bin/psql/command.c | 20 +++++ src/bin/psql/common.c | 131 ++++++++++++++++++++++++++++- src/bin/psql/help.c | 3 +- src/bin/psql/settings.h | 3 +- src/bin/psql/tab-complete.c | 2 +- src/test/regress/expected/psql.out | 85 +++++++++++++++++++ src/test/regress/sql/psql.sql | 36 ++++++++ 8 files changed, 293 insertions(+), 6 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index fd2ca15d0a..5bdbc1e9cf 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1949,6 +1949,25 @@ Tue Oct 26 21:40:57 CEST 1999 + + \gdesc + + + + 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. + + + + If the current query buffer is empty, the most recently sent query + is described instead. + + + + + \gexec diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 4283bf35af..fe0b83ea24 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -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 */ diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index a41932ff27..b99705886f 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -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); diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 9d366180af..4d1c0ec3c6 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -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")); diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index b78f151acd..96338c3197 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -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 */ diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 1583cfa998..7959f9ac16 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -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", diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 4aaf4c1620..7957268388 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -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) diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 4a676c3119..0556b7c159 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -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);