]> granicus.if.org Git - postgresql/commitdiff
Add \gdesc psql command.
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 5 Sep 2017 22:17:47 +0000 (18:17 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 5 Sep 2017 22:17:47 +0000 (18:17 -0400)
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
src/bin/psql/command.c
src/bin/psql/common.c
src/bin/psql/help.c
src/bin/psql/settings.h
src/bin/psql/tab-complete.c
src/test/regress/expected/psql.out
src/test/regress/sql/psql.sql

index fd2ca15d0aaf904e19f0a85fc2e4ab4255242e13..5bdbc1e9cf224b74d96cbd76161c49b7a504eff1 100644 (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>
 
index 4283bf35af4ffb2ed3c192227c7486ede4d8f015..fe0b83ea24c3a8cb1bccd40ab737e32f666472b0 100644 (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
  */
index a41932ff27575ba2edbeeff06560bde8b8ae66ce..b99705886fa8b72df917339a78e35063b59a5941 100644 (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);
 
index 9d366180af5c870740b524c55f653352c5b55daa..4d1c0ec3c68e1cc9680a4c9f898b997bdeddc2d9 100644 (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"));
index b78f151acd8879bcd8864482d2196caf88968b66..96338c31976a0a4aaf6ce9489ebc239f0ad46fe2 100644 (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 */
 
index 1583cfa998e8d0a21d189f2156cf01a57e453f78..7959f9ac16ec820b778b370e17240ff64330c133 100644 (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",
index 4aaf4c162010a674fcddc912e76fea0118feae0c..7957268388f2c8dad3d26b50a1b739616971f494 100644 (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)
index 4a676c3119558d3c2d01f23fde42598cdfe01226..0556b7c159c29f31cadbdaa7636c07e1259a3633 100644 (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);