Hello,
Here is a rebased and updated version of adding \gi:
- improved documentation
- some refactoring to share one function
- signal trap disabled on \gi as well
The thread subject is now a misnomer, I'm not sure whether I should
update it or start a new thread.
On 05/04/2025 22:14, Fabien Coelho wrote:
Hello Tom and Corey,
[...] Anyway, my feeling about it is that \copy parsing is a huge hack
right now, and I'd rather see it become less of a hack, that is
more like other psql commands, instead of getting even hackier.
After giving it some thoughts, I concluded that trying to salvage
\copy is not the
way to go and I have followed Corey's suggestion to extend the
standard SQL COPY
handling by providing an alternate input stream with "\gi file" or
"\gi cmd|".
This has lead to significant restructuring so as to simplify \copy
handling to use
the \g and \gi existing infrastructure. I've moved checked performed
only for \copy
so that they are now also done with \g, and error messages are more
systematically
shown. The pipe char used to mark a command instead of a file is
switched to a
boolean, which is more consistent with other places and how it can be
managed with
"\gi command|" as the pipe char is at the end instead of the start.
The patch also
includes tests and some doc.
If this is accepted, ISTM that \copy could be retired and even removed
at a
later stage, which would solve elegantly its debatable implementation.
--
Fabien.
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f7c8bc16a7f..8f2506517db 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1172,10 +1172,13 @@ SELECT $1 \parse stmt1
<tip>
<para>
Another way to obtain the same result as <literal>\copy
- ... to</literal> is to use the <acronym>SQL</acronym> <literal>COPY
- ... TO STDOUT</literal> command and terminate it
- with <literal>\g <replaceable>filename</replaceable></literal>
- or <literal>\g |<replaceable>program</replaceable></literal>.
+ ... to</literal> or <literal>from</literal> is to use the <acronym>SQL</acronym>
+ <literal>COPY ... TO STDOUT</literal> or <literal>FROM STDIN</literal>
+ command and terminate it with either
+ <literal>\g <replaceable>filename</replaceable></literal>
+ or <literal>\g |<replaceable>program</replaceable></literal> for output
+ and <literal>\gi <replaceable>filename</replaceable></literal>
+ or <literal>\gi <replaceable>program</replaceable>|</literal> for input.
Unlike <literal>\copy</literal>, this method allows the command to
span multiple lines; also, variable interpolation and backquote
expansion can be used.
@@ -1188,7 +1191,7 @@ SELECT $1 \parse stmt1
<command>COPY</command> command with a file or program data source or
destination, because all data must pass through the client/server
connection. For large amounts of data the <acronym>SQL</acronym>
- command might be preferable.
+ command might be preferable if data are available on the server.
</para>
</tip>
@@ -2558,6 +2561,28 @@ CREATE INDEX
</varlistentry>
+ <varlistentry id="app-psql-meta-command-gi">
+ <term><literal>\gi <replaceable class="parameter">file</replaceable></literal></term>
+ <term><literal>\gi <replaceable class="parameter">command</replaceable>|</literal></term>
+ <listitem>
+ <para>
+ Sends the current query buffer to the server and uses the provided
+ <replaceable>file</replaceable> contents or <replaceable>command</replaceable>
+ output as input.
+ This should only apply to <acronym>SQL</acronym>
+ <link linkend="sql-copy"><command>COPY</command></link>
+ which seeks an input when used with <literal>FROM STDIN</literal>, and
+ will simply result in the command simple execution for other commands
+ which do not need an input stream.
+ </para>
+ <para>
+ This approach should be prefered to using <literal>\copy</literal>
+ as it achieves the same result but can span several lines and
+ is subject to variable interpolation and backquote expansion.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="app-psql-meta-command-gset">
<term><literal>\gset [ <replaceable class="parameter">prefix</replaceable> ]</literal></term>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a8a13c2b88b..fd10e458eb1 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -107,6 +107,7 @@ static backslashResult exec_command_getenv(PsqlScanState scan_state, bool active
const char *cmd);
static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_getresults(PsqlScanState scan_state, bool active_branch);
+static backslashResult exec_command_gi(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);
static backslashResult exec_command_html(PsqlScanState scan_state, bool active_branch);
@@ -380,6 +381,8 @@ exec_command(const char *cmd,
status = exec_command_getresults(scan_state, active_branch);
else if (strcmp(cmd, "gexec") == 0)
status = exec_command_gexec(scan_state, active_branch);
+ else if (strcmp(cmd, "gi") == 0)
+ status = exec_command_gi(scan_state, active_branch);
else if (strcmp(cmd, "gset") == 0)
status = exec_command_gset(scan_state, active_branch);
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
@@ -1750,7 +1753,8 @@ exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd)
else
{
expand_tilde(&fname);
- pset.gfname = pg_strdup(fname);
+ pset.g_pipe = fname[0] == '|';
+ pset.gfname = pg_strdup(fname + (pset.g_pipe ? 1 : 0));
}
if (strcmp(cmd, "gx") == 0)
{
@@ -1957,6 +1961,56 @@ exec_command_gexec(PsqlScanState scan_state, bool active_branch)
return status;
}
+/*
+ * \gi filename/shell-command
+ *
+ * Send the current query with a query input from the filename or pipe
+ * command.
+ */
+static backslashResult
+exec_command_gi(PsqlScanState scan_state, bool active_branch)
+{
+ backslashResult status = PSQL_CMD_SKIP_LINE;
+
+ if (active_branch)
+ {
+ char *fname;
+ int last;
+
+ fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, false);
+
+ if (fname == NULL)
+ {
+ pg_log_error("\\gi expects a filename or pipe command");
+ clean_extended_state();
+ free(fname);
+ return PSQL_CMD_ERROR;
+ }
+
+ /* check and truncate final pipe character */
+ last = strlen(fname) - 1;
+ pset.gi_pipe = last >= 0 && fname[last] == '|';
+ if (pset.gi_pipe)
+ fname[last] = '\0';
+
+ if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF)
+ {
+ pg_log_error("\\gi not allowed in pipeline mode");
+ clean_extended_state();
+ free(fname);
+ return PSQL_CMD_ERROR;
+ }
+
+ expand_tilde(&fname);
+ pset.gi_fname = pg_strdup(fname);
+
+ status = PSQL_CMD_SEND;
+ free(fname);
+ }
+
+ return status;
+}
+
/*
* \gset [prefix] -- send query and store result into variables
*/
@@ -2440,9 +2494,10 @@ exec_command_out(PsqlScanState scan_state, bool active_branch)
{
char *fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, true);
+ bool is_pipe = *fname == '|';
expand_tilde(&fname);
- success = setQFout(fname);
+ success = setQFout(fname + (is_pipe ? 1 : 0), is_pipe);
free(fname);
}
else
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 5249336bcf2..e97cb136346 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -12,6 +12,7 @@
#include <math.h>
#include <pwd.h>
#include <signal.h>
+#include <sys/stat.h>
#ifndef WIN32
#include <unistd.h> /* for write() */
#else
@@ -41,35 +42,53 @@ static int ExecQueryAndProcessResults(const char *query,
static bool command_no_begin(const char *query);
+/* make sure the file stream is not a directory */
+static bool
+badFileStream(FILE *file, const char *fname)
+{
+ struct stat st;
+ int result;
+ bool bad;
+
+ if ((result = fstat(fileno(file), &st)) < 0)
+ pg_log_error("could not stat file \"%s\": %m",
+ fname ? fname : "<?>");
+
+ if (result == 0 && S_ISDIR(st.st_mode))
+ pg_log_error("cannot copy from/to directory \"%s\"",
+ fname ? fname : "<?>");
+
+ return result < 0 || S_ISDIR(st.st_mode);
+}
+
/*
* openQueryOutputFile --- attempt to open a query output file
*
- * fname == NULL selects stdout, else an initial '|' selects a pipe,
- * else plain file.
- *
- * Returns output file pointer into *fout, and is-a-pipe flag into *is_pipe.
+ * Returns output file pointer into *fout.
* Caller is responsible for adjusting SIGPIPE state if it's a pipe.
*
* On error, reports suitable error message and returns false.
*/
bool
-openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe)
+openQueryOutputFile(const char *fname, bool is_pipe, FILE **fout)
{
if (!fname || fname[0] == '\0')
- {
*fout = stdout;
- *is_pipe = false;
- }
- else if (*fname == '|')
+ else if (is_pipe)
{
fflush(NULL);
- *fout = popen(fname + 1, "w");
- *is_pipe = true;
+ *fout = popen(fname, PG_BINARY_W);
}
else
{
- *fout = fopen(fname, "w");
- *is_pipe = false;
+ *fout = fopen(fname, PG_BINARY_W);
+
+ if (*fout && badFileStream(*fout, fname))
+ {
+ fclose(*fout);
+ *fout = NULL;
+ return false;
+ }
}
if (*fout == NULL)
@@ -86,15 +105,15 @@ openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe)
* open it and update the caller's gfile_fout and is_pipe state variables.
* Return true if OK, false if an error occurred.
*/
-static bool
-SetupGOutput(FILE **gfile_fout, bool *is_pipe)
+bool
+SetupGOutput(FILE **gfile_fout)
{
/* If there is a \g file or program, and it's not already open, open it */
if (pset.gfname != NULL && *gfile_fout == NULL)
{
- if (openQueryOutputFile(pset.gfname, gfile_fout, is_pipe))
+ if (openQueryOutputFile(pset.gfname, pset.g_pipe, gfile_fout))
{
- if (*is_pipe)
+ if (pset.g_pipe)
disable_sigpipe_trap();
}
else
@@ -104,21 +123,107 @@ SetupGOutput(FILE **gfile_fout, bool *is_pipe)
}
/*
- * Close the output stream for \g, if we opened it.
+ * Close file with user feedback on errors.
+ */
+static bool
+CloseFile(FILE *stream, const char *fname)
+{
+ if (fclose(stream) != 0)
+ {
+ pg_log_error("%s: %m", fname);
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Close pipe with user feedback on errors.
+ */
+static bool
+ClosePipe(FILE *stream, const char *fname)
+{
+ int pclose_rc = pclose(stream);
+
+ if (pclose_rc != 0)
+ {
+ if (pclose_rc < 0)
+ pg_log_error("could not close pipe to/from external command: %m");
+ else
+ {
+ char *reason = wait_result_to_str(pclose_rc);
+
+ pg_log_error("%s: %s", fname ? fname: "<?>", reason ? reason : "");
+ free(reason);
+ }
+ return false;
+ }
+
+ SetShellResultVariables(pclose_rc);
+ restore_sigpipe_trap();
+ return true;
+}
+
+/*
+ * Close the input (\gi) or output (\g) stream, if we opened it.
*/
static void
-CloseGOutput(FILE *gfile_fout, bool is_pipe)
+CloseStream(FILE *stream, const char *fname, bool is_pipe)
{
- if (gfile_fout)
+ if (fname && stream)
{
if (is_pipe)
+ (void) ClosePipe(stream, fname);
+ else
+ (void) CloseFile(stream, fname);
+ }
+}
+
+/*
+ * Open or use input stream, only under COPY_IN (COPY or \copy)
+ */
+bool
+SetupGInput(FILE **input_stream)
+{
+ if (pset.gi_fname != NULL && *input_stream == NULL)
+ {
+ FILE *input = NULL;
+
+ if (pset.gi_pipe)
{
- SetShellResultVariables(pclose(gfile_fout));
- restore_sigpipe_trap();
+ fflush(NULL);
+ errno = 0;
+ input = popen(pset.gi_fname, PG_BINARY_R);
+ if (!input)
+ {
+ pg_log_error("could not execute command \"%s\": %m",
+ pset.gi_fname);
+ return false;
+ }
+ disable_sigpipe_trap();
}
else
- fclose(gfile_fout);
+ {
+ input = fopen(pset.gi_fname, PG_BINARY_R);
+ if (!input)
+ {
+ pg_log_error("could not open file \"%s\": %m", pset.gi_fname);
+ return false;
+ }
+ if (input && badFileStream(input, pset.gi_fname))
+ {
+ fclose(input);
+ return false;
+ }
+ }
+
+ *input_stream = input;
}
+ else if (pset.copy_pstd)
+ *input_stream = pset.cur_cmd_source;
+ else
+ *input_stream = stdin;
+
+ return true;
}
/*
@@ -141,25 +246,31 @@ pipelineReset(void)
* On failure, returns false without changing pset state.
*/
bool
-setQFout(const char *fname)
+setQFout(const char *fname, bool is_pipe)
{
FILE *fout;
- bool is_pipe;
/* First make sure we can open the new output file/pipe */
- if (!openQueryOutputFile(fname, &fout, &is_pipe))
+ if (!openQueryOutputFile(fname, is_pipe, &fout))
return false;
/* Close old file/pipe */
if (pset.queryFout && pset.queryFout != stdout && pset.queryFout != stderr)
{
if (pset.queryFoutPipe)
- SetShellResultVariables(pclose(pset.queryFout));
+ ClosePipe(pset.queryFout, pset.queryFName);
else
- fclose(pset.queryFout);
+ CloseFile(pset.queryFout, pset.queryFName);
+
+ if (pset.queryFName)
+ {
+ free(pset.queryFName);
+ pset.queryFName = NULL;
+ }
}
pset.queryFout = fout;
+ pset.queryFName = fname ? pg_strdup(fname) : NULL;
pset.queryFoutPipe = is_pipe;
/* Adjust SIGPIPE handling appropriately: ignore signal if is_pipe */
@@ -923,10 +1034,6 @@ loop_exit:
* connection out of its COPY state, then call PQresultStatus()
* once and report any error. Return whether all was ok.
*
- * For COPY OUT, direct the output to copystream, or discard if that's NULL.
- * For COPY IN, use pset.copyStream as data source if it's set,
- * otherwise cur_cmd_source.
- *
* Update *resultp if further processing is necessary; set to NULL otherwise.
* Return a result when queryFout can safely output a result status: on COPY
* IN, or on COPY OUT if written to something other than pset.queryFout.
@@ -966,8 +1073,6 @@ HandleCopyResult(PGresult **resultp, FILE *copystream)
else
{
/* COPY IN */
- /* Ignore the copystream argument passed to the function */
- copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
success = handleCopyIn(pset.db,
copystream,
PQbinaryTuples(*resultp),
@@ -1303,6 +1408,13 @@ sendquery_cleanup:
pset.gfname = NULL;
}
+ /* idem \gi */
+ if (pset.gi_fname)
+ {
+ free(pset.gi_fname);
+ pset.gi_fname = NULL;
+ }
+
/* restore print settings if \g changed them */
if (pset.gsavepopt)
{
@@ -1548,8 +1660,8 @@ ExecQueryAndProcessResults(const char *query,
instr_time before,
after;
PGresult *result;
- FILE *gfile_fout = NULL;
- bool gfile_is_pipe = false;
+ FILE *gfile_fout = NULL,
+ *gfile_fin = NULL;
if (timing)
INSTR_TIME_SET_CURRENT(before);
@@ -1852,9 +1964,8 @@ ExecQueryAndProcessResults(const char *query,
/*
* For COPY OUT, direct the output to the default place (probably
- * a pager pipe) for \watch, or to pset.copyStream for \copy,
- * otherwise to pset.gfname if that's set, otherwise to
- * pset.queryFout.
+ * a pager pipe) for \watch, or use to pset.gfname if that's set,
+ * otherwise to pset.queryFout.
*/
if (result_status == PGRES_COPY_OUT)
{
@@ -1865,15 +1976,15 @@ ExecQueryAndProcessResults(const char *query,
}
else if (pset.copyStream)
{
- /* invoked by \copy */
+ /* \copy ... to ... */
copy_stream = pset.copyStream;
}
else if (pset.gfname)
{
- /* COPY followed by \g filename or \g |program */
- success &= SetupGOutput(&gfile_fout, &gfile_is_pipe);
- if (gfile_fout)
- copy_stream = gfile_fout;
+ /* COPY with \g filename or \g |program */
+ if (!gfile_fout)
+ success &= SetupGOutput(&gfile_fout);
+ copy_stream = gfile_fout;
}
else
{
@@ -1881,10 +1992,25 @@ ExecQueryAndProcessResults(const char *query,
copy_stream = pset.queryFout;
}
}
+ else if (result_status == PGRES_COPY_IN)
+ {
+ if (pset.copyStream)
+ {
+ /* \copy ... from ... */
+ copy_stream = pset.copyStream;
+ }
+ else
+ {
+ /* COPY with or without \gi ... */
+ if (!gfile_fin)
+ success &= SetupGInput(&gfile_fin);
+ copy_stream = gfile_fin;
+ }
+ }
/*
- * Even if the output stream could not be opened, we call
- * HandleCopyResult() with a NULL output stream to collect and
+ * Even if the input or output stream could not be opened, we call
+ * HandleCopyResult() with a NULL stream to collect and
* discard the COPY data.
*/
success &= HandleCopyResult(&result, copy_stream);
@@ -1905,7 +2031,7 @@ ExecQueryAndProcessResults(const char *query,
my_popt.topt.prior_records = 0;
/* open \g file if needed */
- success &= SetupGOutput(&gfile_fout, &gfile_is_pipe);
+ success &= SetupGOutput(&gfile_fout);
if (gfile_fout)
tuples_fout = gfile_fout;
@@ -2094,7 +2220,7 @@ ExecQueryAndProcessResults(const char *query,
FILE *tuples_fout = printQueryFout;
if (PQresultStatus(result) == PGRES_TUPLES_OK)
- success &= SetupGOutput(&gfile_fout, &gfile_is_pipe);
+ success &= SetupGOutput(&gfile_fout);
if (gfile_fout)
tuples_fout = gfile_fout;
if (success)
@@ -2124,8 +2250,9 @@ ExecQueryAndProcessResults(const char *query,
}
}
- /* close \g file if we opened it */
- CloseGOutput(gfile_fout, gfile_is_pipe);
+ /* close \g and \gi files if we opened one */
+ CloseStream(gfile_fout, pset.gfname, pset.g_pipe);
+ CloseStream(gfile_fin, pset.gi_fname, pset.gi_pipe);
if (end_pipeline)
{
diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h
index 7f1a23de1e8..38fb6bf66a9 100644
--- a/src/bin/psql/common.h
+++ b/src/bin/psql/common.h
@@ -15,8 +15,10 @@
#include "fe_utils/psqlscan.h"
#include "libpq-fe.h"
-extern bool openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe);
-extern bool setQFout(const char *fname);
+extern bool openQueryOutputFile(const char *fname, bool is_pipe, FILE **fout);
+extern bool SetupGOutput(FILE **output);
+extern bool SetupGInput(FILE **input);
+extern bool setQFout(const char *fname, bool is_pipe);
extern char *psql_get_variable(const char *varname, PsqlScanQuoteType quote,
void *passthrough);
diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c
index 92c955b637a..7ee221e8188 100644
--- a/src/bin/psql/copy.c
+++ b/src/bin/psql/copy.c
@@ -8,7 +8,6 @@
#include "postgres_fe.h"
#include <signal.h>
-#include <sys/stat.h>
#ifndef WIN32
#include <unistd.h> /* for isatty */
#else
@@ -18,6 +17,7 @@
#include "common.h"
#include "common/logging.h"
#include "copy.h"
+#include "common.h"
#include "libpq-fe.h"
#include "pqexpbuffer.h"
#include "prompt.h"
@@ -268,7 +268,6 @@ bool
do_copy(const char *args)
{
PQExpBufferData query;
- FILE *copystream;
struct copy_options *options;
bool success;
@@ -282,81 +281,43 @@ do_copy(const char *args)
if (options->file && !options->program)
canonicalize_path_enc(options->file, pset.encoding);
+ /* \copy with pstdout/pstdin vs stdout/stdin */
+ pset.copy_pstd = options->psql_inout;
+
+ /*
+ * Translate \copy destination as \g or source as \gi.
+ *
+ * The stream is opened to return early if there is some error,
+ * which means that any error at this level does **not** break
+ * the current transaction.
+ * If okay, the stream is closed in SendQuery.
+ */
if (options->from)
{
if (options->file)
{
- if (options->program)
- {
- fflush(NULL);
- errno = 0;
- copystream = popen(options->file, PG_BINARY_R);
- }
- else
- copystream = fopen(options->file, PG_BINARY_R);
+ pset.gi_fname = pg_strdup(options->file);
+ pset.gi_pipe = options->program;
}
- else if (!options->psql_inout)
- copystream = pset.cur_cmd_source;
- else
- copystream = stdin;
+ SetupGInput(&pset.copyStream);
+ if (!pset.copyStream)
+ return false;
}
else
{
if (options->file)
{
- if (options->program)
- {
- fflush(NULL);
- disable_sigpipe_trap();
- errno = 0;
- copystream = popen(options->file, PG_BINARY_W);
- }
- else
- copystream = fopen(options->file, PG_BINARY_W);
+ pset.gfname = pg_strdup(options->file);
+ pset.g_pipe = options->program;
}
- else if (!options->psql_inout)
- copystream = pset.queryFout;
- else
- copystream = stdout;
- }
-
- if (!copystream)
- {
- if (options->program)
- pg_log_error("could not execute command \"%s\": %m",
- options->file);
- else
- pg_log_error("%s: %m",
- options->file);
- free_copy_options(options);
- return false;
- }
-
- if (!options->program)
- {
- struct stat st;
- int result;
-
- /* make sure the specified file is not a directory */
- if ((result = fstat(fileno(copystream), &st)) < 0)
- pg_log_error("could not stat file \"%s\": %m",
- options->file);
-
- if (result == 0 && S_ISDIR(st.st_mode))
- pg_log_error("%s: cannot copy from/to a directory",
- options->file);
-
- if (result < 0 || S_ISDIR(st.st_mode))
- {
- fclose(copystream);
- free_copy_options(options);
+ SetupGOutput(&pset.copyStream);
+ if (!pset.copyStream)
return false;
- }
}
/* build the command we will send to the backend */
initPQExpBuffer(&query);
- printfPQExpBuffer(&query, "COPY ");
+ appendPQExpBuffer(&query, "COPY ");
appendPQExpBufferStr(&query, options->before_tofrom);
if (options->from)
appendPQExpBufferStr(&query, " FROM STDIN ");
@@ -365,44 +326,11 @@ do_copy(const char *args)
if (options->after_tofrom)
appendPQExpBufferStr(&query, options->after_tofrom);
- /* run it like a user command, but with copystream as data source/sink */
- pset.copyStream = copystream;
+ /* run it like a user command */
success = SendQuery(query.data);
+
pset.copyStream = NULL;
termPQExpBuffer(&query);
-
- if (options->file != NULL)
- {
- if (options->program)
- {
- int pclose_rc = pclose(copystream);
-
- if (pclose_rc != 0)
- {
- if (pclose_rc < 0)
- pg_log_error("could not close pipe to external command: %m");
- else
- {
- char *reason = wait_result_to_str(pclose_rc);
-
- pg_log_error("%s: %s", options->file,
- reason ? reason : "");
- free(reason);
- }
- success = false;
- }
- SetShellResultVariables(pclose_rc);
- restore_sigpipe_trap();
- }
- else
- {
- if (fclose(copystream) != 0)
- {
- pg_log_error("%s: %m", options->file);
- success = false;
- }
- }
- }
free_copy_options(options);
return success;
}
@@ -514,6 +442,13 @@ handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res)
char buf[COPYBUFSIZ];
bool showprompt;
+ /* No input stream on COPY ... \gi 'non-such-file' */
+ if (copystream == NULL)
+ {
+ OK = false;
+ goto copyin_cleanup;
+ }
+
/*
* Establish longjmp destination for exiting from wait-for-input. (This is
* only effective while sigint_interrupt_enabled is TRUE.)
@@ -703,7 +638,8 @@ copyin_cleanup:
* with feof(), some fread() implementations won't read more data if it's
* set. This also clears the error flag, but we already checked that.
*/
- clearerr(copystream);
+ if (copystream)
+ clearerr(copystream);
/*
* Check command status and return to normal libpq state.
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index daa725246c9..ae45a0a2c90 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -177,6 +177,7 @@ slashUsage(unsigned short int pager)
HELP0(" \\getresults [NUM_RES] read NUM_RES pending results. All pending results are\n"
" read if no argument is provided\n");
HELP0(" \\gexec execute query, then execute each value in its result\n");
+ HELP0(" \\gi FILE execute query, reading from file or pipe| if needed\n");
HELP0(" \\gset [PREFIX] execute query and store result in psql variables\n");
HELP0(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
HELP0(" \\parse STMT_NAME create a prepared statement\n");
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index fd82303f776..683d90e6fc5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -102,18 +102,24 @@ typedef struct _psqlSettings
{
PGconn *db; /* connection to backend */
int encoding; /* client_encoding */
+
FILE *queryFout; /* where to send the query results */
+ char *queryFName; /* name of above Fout stream */
bool queryFoutPipe; /* queryFout is from a popen() */
- FILE *copyStream; /* Stream to read/write for \copy command */
-
PGresult *last_error_result; /* most recent error result, if any */
printQueryOpt popt; /* The active print format settings */
-
- char *gfname; /* one-shot file output argument for \g */
printQueryOpt *gsavepopt; /* if not null, saved print format settings */
+ char *gfname; /* one-shot output argument for \g */
+ bool g_pipe; /* whether \g is to a pipe or file */
+ char *gi_fname; /* one-shot input argument for \gi */
+ bool gi_pipe; /* whether \gi is from a pipe or file */
+
+ bool copy_pstd; /* \copy pstdout/pstdin vs stdout/stdin */
+ FILE *copyStream; /* current \copy to/from file/pipe */
+
char *gset_prefix; /* one-shot prefix argument for \gset */
bool gdesc_flag; /* one-shot request to describe query result */
bool gexec_flag; /* one-shot request to execute query result */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 249b6aa5169..158ccd13929 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -474,7 +474,7 @@ error:
PQfinish(pset.db);
if (pset.dead_conn)
PQfinish(pset.dead_conn);
- setQFout(NULL);
+ setQFout(NULL, false);
return successResult;
}
@@ -591,9 +591,13 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
options->no_readline = true;
break;
case 'o':
- if (!setQFout(optarg))
- exit(EXIT_FAILURE);
- break;
+ {
+ bool is_pipe = *optarg == '|';
+
+ if (!setQFout(optarg + (is_pipe ? 1 : 0), is_pipe))
+ exit(EXIT_FAILURE);
+ break;
+ }
case 'p':
options->port = pg_strdup(optarg);
break;
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 739cb439708..5fff7352dc2 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -481,4 +481,40 @@ psql_like($node, "copy (values ('foo'),('bar')) to stdout \\g | $pipe_cmd",
my $c4 = slurp_file($g_file);
like($c4, qr/foo.*bar/s);
+# Test \gi file
+my $gi_file = "$tempdir/gi_file.in";
+append_to_file($gi_file, "Susie\nCalvin\nHobbes\n");
+psql_like(
+ $node,
+ "CREATE TABLE gi_data(stuff TEXT);\n" .
+ "COPY gi_data(stuff) FROM STDIN \\gi '$gi_file'\n" .
+ "SELECT stuff FROM gi_data ORDER BY 1;\n",
+ qr/Calvin.*Hobbes.*Susie/s,
+ "COPY ... \\gi file");
+
+psql_like(
+ $node,
+ "COPY gi_data FROM STDIN \\gi '$perlbin -e \"print qq{Rosalyn\\n}\"|'\n" .
+ "SELECT * FROM gi_data WHERE stuff ILIKE '%sal%';",
+ qr/Rosalyn/,
+ "COPY ... \\gi command");
+
+psql_like(
+ $node,
+ "SELECT 'hello' \\gi '$gi_file'\n",
+ qr/hello/,
+ "SELECT ... \\gi file is simply executed");
+
+psql_fails_like(
+ $node,
+ "SELECT 'missing file' \\gi\n",
+ qr/\\gi expects a filename or pipe command/,
+ "missing file parameter to \\gi");
+
+psql_fails_like(
+ $node,
+ "COPY gi_data(stuff) FROM STDIN \\gi '$tempdir/no-such-file'\n",
+ qr/No such file or directory/,
+ "COPY ... \\gi no-such-file");
+
done_testing();
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c916b9299a8..dd1c04346e1 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1887,7 +1887,8 @@ psql_completion(const char *text, int start, int end)
"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
"\\endif", "\\endpipeline", "\\errverbose", "\\ev",
"\\f", "\\flush", "\\flushrequest",
- "\\g", "\\gdesc", "\\getenv", "\\getresults", "\\gexec", "\\gset", "\\gx",
+ "\\g", "\\gdesc", "\\getenv", "\\getresults", "\\gexec", "\\gi",
+ "\\gset", "\\gx",
"\\help", "\\html",
"\\if", "\\include", "\\include_relative", "\\ir",
"\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
@@ -5440,7 +5441,7 @@ match_previous_words(int pattern_id,
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines);
else if (TailMatchesCS("\\sv*"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views);
- else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|"
+ else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\gi|\\i|\\include|"
"\\ir|\\include_relative|\\o|\\out|"
"\\s|\\w|\\write|\\lo_import"))
{