
Per discussion with David Christensen, there can be multiple instances of PG accessible via local sockets, and you need the port to see which one you're actually connected to. David's original patch worked this way, but I inadvertently ripped it out during commit.
2227 lines
49 KiB
C
2227 lines
49 KiB
C
/*
|
|
* psql - the PostgreSQL interactive terminal
|
|
*
|
|
* Copyright (c) 2000-2010, PostgreSQL Global Development Group
|
|
*
|
|
* $PostgreSQL: pgsql/src/bin/psql/command.c,v 1.223 2010/07/20 14:14:30 rhaas Exp $
|
|
*/
|
|
#include "postgres_fe.h"
|
|
#include "command.h"
|
|
|
|
#ifdef __BORLANDC__ /* needed for BCC */
|
|
#undef mkdir
|
|
#endif
|
|
|
|
#include <ctype.h>
|
|
#ifdef HAVE_PWD_H
|
|
#include <pwd.h>
|
|
#endif
|
|
#ifndef WIN32
|
|
#include <sys/types.h> /* for umask() */
|
|
#include <sys/stat.h> /* for stat() */
|
|
#include <fcntl.h> /* open() flags */
|
|
#include <unistd.h> /* for geteuid(), getpid(), stat() */
|
|
#else
|
|
#include <win32.h>
|
|
#include <io.h>
|
|
#include <fcntl.h>
|
|
#include <direct.h>
|
|
#include <sys/types.h> /* for umask() */
|
|
#include <sys/stat.h> /* for stat() */
|
|
#endif
|
|
#ifdef USE_SSL
|
|
#include <openssl/ssl.h>
|
|
#endif
|
|
|
|
#include "portability/instr_time.h"
|
|
|
|
#include "libpq-fe.h"
|
|
#include "pqexpbuffer.h"
|
|
#include "dumputils.h"
|
|
|
|
#include "common.h"
|
|
#include "copy.h"
|
|
#include "describe.h"
|
|
#include "help.h"
|
|
#include "input.h"
|
|
#include "large_obj.h"
|
|
#include "mainloop.h"
|
|
#include "print.h"
|
|
#include "psqlscan.h"
|
|
#include "settings.h"
|
|
#include "variables.h"
|
|
|
|
|
|
/* functions for use in this file */
|
|
static backslashResult exec_command(const char *cmd,
|
|
PsqlScanState scan_state,
|
|
PQExpBuffer query_buf);
|
|
static bool do_edit(const char *filename_arg, PQExpBuffer query_buf,
|
|
bool *edited);
|
|
static bool do_connect(char *dbname, char *user, char *host, char *port);
|
|
static bool do_shell(const char *command);
|
|
static bool lookup_function_oid(PGconn *conn, const char *desc, Oid *foid);
|
|
static bool get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf);
|
|
static void minimal_error_message(PGresult *res);
|
|
|
|
static void printSSLInfo(void);
|
|
|
|
#ifdef WIN32
|
|
static void checkWin32Codepage(void);
|
|
#endif
|
|
|
|
|
|
|
|
/*----------
|
|
* HandleSlashCmds:
|
|
*
|
|
* Handles all the different commands that start with '\'.
|
|
* Ordinarily called by MainLoop().
|
|
*
|
|
* scan_state is a lexer working state that is set to continue scanning
|
|
* just after the '\'. The lexer is advanced past the command and all
|
|
* arguments on return.
|
|
*
|
|
* 'query_buf' contains the query-so-far, which may be modified by
|
|
* execution of the backslash command (for example, \r clears it).
|
|
* query_buf can be NULL if there is no query so far.
|
|
*
|
|
* Returns a status code indicating what action is desired, see command.h.
|
|
*----------
|
|
*/
|
|
|
|
backslashResult
|
|
HandleSlashCmds(PsqlScanState scan_state,
|
|
PQExpBuffer query_buf)
|
|
{
|
|
backslashResult status = PSQL_CMD_SKIP_LINE;
|
|
char *cmd;
|
|
char *arg;
|
|
|
|
psql_assert(scan_state);
|
|
|
|
/* Parse off the command name */
|
|
cmd = psql_scan_slash_command(scan_state);
|
|
|
|
/* And try to execute it */
|
|
status = exec_command(cmd, scan_state, query_buf);
|
|
|
|
if (status == PSQL_CMD_UNKNOWN)
|
|
{
|
|
if (pset.cur_cmd_interactive)
|
|
fprintf(stderr, _("Invalid command \\%s. Try \\? for help.\n"), cmd);
|
|
else
|
|
psql_error("invalid command \\%s\n", cmd);
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
|
|
if (status != PSQL_CMD_ERROR)
|
|
{
|
|
/* eat any remaining arguments after a valid command */
|
|
/* note we suppress evaluation of backticks here */
|
|
while ((arg = psql_scan_slash_option(scan_state,
|
|
OT_VERBATIM, NULL, false)))
|
|
{
|
|
psql_error("\\%s: extra argument \"%s\" ignored\n", cmd, arg);
|
|
free(arg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* silently throw away rest of line after an erroneous command */
|
|
while ((arg = psql_scan_slash_option(scan_state,
|
|
OT_WHOLE_LINE, NULL, false)))
|
|
free(arg);
|
|
}
|
|
|
|
/* if there is a trailing \\, swallow it */
|
|
psql_scan_slash_command_end(scan_state);
|
|
|
|
free(cmd);
|
|
|
|
/* some commands write to queryFout, so make sure output is sent */
|
|
fflush(pset.queryFout);
|
|
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
* Read and interpret an argument to the \connect slash command.
|
|
*/
|
|
static char *
|
|
read_connect_arg(PsqlScanState scan_state)
|
|
{
|
|
char *result;
|
|
char quote;
|
|
|
|
/*
|
|
* Ideally we should treat the arguments as SQL identifiers. But for
|
|
* backwards compatibility with 7.2 and older pg_dump files, we have to
|
|
* take unquoted arguments verbatim (don't downcase them). For now,
|
|
* double-quoted arguments may be stripped of double quotes (as if SQL
|
|
* identifiers). By 7.4 or so, pg_dump files can be expected to
|
|
* double-quote all mixed-case \connect arguments, and then we can get rid
|
|
* of OT_SQLIDHACK.
|
|
*/
|
|
result = psql_scan_slash_option(scan_state, OT_SQLIDHACK, "e, true);
|
|
|
|
if (!result)
|
|
return NULL;
|
|
|
|
if (quote)
|
|
return result;
|
|
|
|
if (*result == '\0' || strcmp(result, "-") == 0)
|
|
return NULL;
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
* Subroutine to actually try to execute a backslash command.
|
|
*/
|
|
static backslashResult
|
|
exec_command(const char *cmd,
|
|
PsqlScanState scan_state,
|
|
PQExpBuffer query_buf)
|
|
{
|
|
bool success = true; /* indicate here if the command ran ok or
|
|
* failed */
|
|
backslashResult status = PSQL_CMD_SKIP_LINE;
|
|
|
|
/*
|
|
* \a -- toggle field alignment This makes little sense but we keep it
|
|
* around.
|
|
*/
|
|
if (strcmp(cmd, "a") == 0)
|
|
{
|
|
if (pset.popt.topt.format != PRINT_ALIGNED)
|
|
success = do_pset("format", "aligned", &pset.popt, pset.quiet);
|
|
else
|
|
success = do_pset("format", "unaligned", &pset.popt, pset.quiet);
|
|
}
|
|
|
|
/* \C -- override table title (formerly change HTML caption) */
|
|
else if (strcmp(cmd, "C") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
success = do_pset("title", opt, &pset.popt, pset.quiet);
|
|
free(opt);
|
|
}
|
|
|
|
/*
|
|
* \c or \connect -- connect to database using the specified parameters.
|
|
*
|
|
* \c dbname user host port
|
|
*
|
|
* If any of these parameters are omitted or specified as '-', the current
|
|
* value of the parameter will be used instead. If the parameter has no
|
|
* current value, the default value for that parameter will be used. Some
|
|
* examples:
|
|
*
|
|
* \c - - hst Connect to current database on current port of host
|
|
* "hst" as current user. \c - usr - prt Connect to current database on
|
|
* "prt" port of current host as user "usr". \c dbs Connect to
|
|
* "dbs" database on current port of current host as current user.
|
|
*/
|
|
else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0)
|
|
{
|
|
char *opt1,
|
|
*opt2,
|
|
*opt3,
|
|
*opt4;
|
|
|
|
opt1 = read_connect_arg(scan_state);
|
|
opt2 = read_connect_arg(scan_state);
|
|
opt3 = read_connect_arg(scan_state);
|
|
opt4 = read_connect_arg(scan_state);
|
|
|
|
success = do_connect(opt1, opt2, opt3, opt4);
|
|
|
|
free(opt1);
|
|
free(opt2);
|
|
free(opt3);
|
|
free(opt4);
|
|
}
|
|
|
|
/* \cd */
|
|
else if (strcmp(cmd, "cd") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
char *dir;
|
|
|
|
if (opt)
|
|
dir = opt;
|
|
else
|
|
{
|
|
#ifndef WIN32
|
|
struct passwd *pw;
|
|
|
|
pw = getpwuid(geteuid());
|
|
if (!pw)
|
|
{
|
|
psql_error("could not get home directory: %s\n", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
dir = pw->pw_dir;
|
|
#else /* WIN32 */
|
|
|
|
/*
|
|
* On Windows, 'cd' without arguments prints the current
|
|
* directory, so if someone wants to code this here instead...
|
|
*/
|
|
dir = "/";
|
|
#endif /* WIN32 */
|
|
}
|
|
|
|
if (chdir(dir) == -1)
|
|
{
|
|
psql_error("\\%s: could not change directory to \"%s\": %s\n",
|
|
cmd, dir, strerror(errno));
|
|
success = false;
|
|
}
|
|
|
|
if (pset.dirname)
|
|
free(pset.dirname);
|
|
pset.dirname = pg_strdup(dir);
|
|
canonicalize_path(pset.dirname);
|
|
|
|
if (opt)
|
|
free(opt);
|
|
}
|
|
|
|
/* \conninfo -- display information about the current connection */
|
|
else if (strcmp(cmd, "conninfo") == 0)
|
|
{
|
|
char *db = PQdb(pset.db);
|
|
char *host = PQhost(pset.db);
|
|
|
|
if (!db)
|
|
printf("You are not connected.\n");
|
|
else if (host)
|
|
printf("You are connected to database \"%s\" on host \"%s\" at port \"%s\" as user \"%s\".\n",
|
|
db, host, PQport(pset.db), PQuser(pset.db));
|
|
else
|
|
printf("You are connected to database \"%s\" via local socket at port \"%s\" as user \"%s\".\n",
|
|
db, PQport(pset.db), PQuser(pset.db));
|
|
}
|
|
|
|
/* \copy */
|
|
else if (pg_strcasecmp(cmd, "copy") == 0)
|
|
{
|
|
/* Default fetch-it-all-and-print mode */
|
|
instr_time before,
|
|
after;
|
|
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_WHOLE_LINE, NULL, false);
|
|
|
|
if (pset.timing)
|
|
INSTR_TIME_SET_CURRENT(before);
|
|
|
|
success = do_copy(opt);
|
|
|
|
if (pset.timing && success)
|
|
{
|
|
INSTR_TIME_SET_CURRENT(after);
|
|
INSTR_TIME_SUBTRACT(after, before);
|
|
printf(_("Time: %.3f ms\n"), INSTR_TIME_GET_MILLISEC(after));
|
|
}
|
|
|
|
free(opt);
|
|
}
|
|
|
|
/* \copyright */
|
|
else if (strcmp(cmd, "copyright") == 0)
|
|
print_copyright();
|
|
|
|
/* \d* commands */
|
|
else if (cmd[0] == 'd')
|
|
{
|
|
char *pattern;
|
|
bool show_verbose,
|
|
show_system;
|
|
|
|
/* We don't do SQLID reduction on the pattern yet */
|
|
pattern = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
show_verbose = strchr(cmd, '+') ? true : false;
|
|
show_system = strchr(cmd, 'S') ? true : false;
|
|
|
|
switch (cmd[1])
|
|
{
|
|
case '\0':
|
|
case '+':
|
|
case 'S':
|
|
if (pattern)
|
|
success = describeTableDetails(pattern, show_verbose, show_system);
|
|
else
|
|
/* standard listing of interesting things */
|
|
success = listTables("tvs", NULL, show_verbose, show_system);
|
|
break;
|
|
case 'a':
|
|
success = describeAggregates(pattern, show_verbose, show_system);
|
|
break;
|
|
case 'b':
|
|
success = describeTablespaces(pattern, show_verbose);
|
|
break;
|
|
case 'c':
|
|
success = listConversions(pattern, show_system);
|
|
break;
|
|
case 'C':
|
|
success = listCasts(pattern);
|
|
break;
|
|
case 'd':
|
|
if (strncmp(cmd, "ddp", 3) == 0)
|
|
success = listDefaultACLs(pattern);
|
|
else
|
|
success = objectDescription(pattern, show_system);
|
|
break;
|
|
case 'D':
|
|
success = listDomains(pattern, show_system);
|
|
break;
|
|
case 'f': /* function subsystem */
|
|
switch (cmd[2])
|
|
{
|
|
case '\0':
|
|
case '+':
|
|
case 'S':
|
|
case 'a':
|
|
case 'n':
|
|
case 't':
|
|
case 'w':
|
|
success = describeFunctions(&cmd[2], pattern, show_verbose, show_system);
|
|
break;
|
|
default:
|
|
status = PSQL_CMD_UNKNOWN;
|
|
break;
|
|
}
|
|
break;
|
|
case 'g':
|
|
/* no longer distinct from \du */
|
|
success = describeRoles(pattern, show_verbose);
|
|
break;
|
|
case 'l':
|
|
success = do_lo_list();
|
|
break;
|
|
case 'n':
|
|
success = listSchemas(pattern, show_verbose);
|
|
break;
|
|
case 'o':
|
|
success = describeOperators(pattern, show_system);
|
|
break;
|
|
case 'p':
|
|
success = permissionsList(pattern);
|
|
break;
|
|
case 'T':
|
|
success = describeTypes(pattern, show_verbose, show_system);
|
|
break;
|
|
case 't':
|
|
case 'v':
|
|
case 'i':
|
|
case 's':
|
|
success = listTables(&cmd[1], pattern, show_verbose, show_system);
|
|
break;
|
|
case 'r':
|
|
if (cmd[2] == 'd' && cmd[3] == 's')
|
|
{
|
|
char *pattern2 = NULL;
|
|
|
|
if (pattern)
|
|
pattern2 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
success = listDbRoleSettings(pattern, pattern2);
|
|
}
|
|
else
|
|
success = PSQL_CMD_UNKNOWN;
|
|
break;
|
|
case 'u':
|
|
success = describeRoles(pattern, show_verbose);
|
|
break;
|
|
case 'F': /* text search subsystem */
|
|
switch (cmd[2])
|
|
{
|
|
case '\0':
|
|
case '+':
|
|
success = listTSConfigs(pattern, show_verbose);
|
|
break;
|
|
case 'p':
|
|
success = listTSParsers(pattern, show_verbose);
|
|
break;
|
|
case 'd':
|
|
success = listTSDictionaries(pattern, show_verbose);
|
|
break;
|
|
case 't':
|
|
success = listTSTemplates(pattern, show_verbose);
|
|
break;
|
|
default:
|
|
status = PSQL_CMD_UNKNOWN;
|
|
break;
|
|
}
|
|
break;
|
|
case 'e': /* SQL/MED subsystem */
|
|
switch (cmd[2])
|
|
{
|
|
case 's':
|
|
success = listForeignServers(pattern, show_verbose);
|
|
break;
|
|
case 'u':
|
|
success = listUserMappings(pattern, show_verbose);
|
|
break;
|
|
case 'w':
|
|
success = listForeignDataWrappers(pattern, show_verbose);
|
|
break;
|
|
default:
|
|
status = PSQL_CMD_UNKNOWN;
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
status = PSQL_CMD_UNKNOWN;
|
|
}
|
|
|
|
if (pattern)
|
|
free(pattern);
|
|
}
|
|
|
|
|
|
/*
|
|
* \e or \edit -- edit the current query buffer (or a file and make it the
|
|
* query buffer
|
|
*/
|
|
else if (strcmp(cmd, "e") == 0 || strcmp(cmd, "edit") == 0)
|
|
{
|
|
if (!query_buf)
|
|
{
|
|
psql_error("no query buffer\n");
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
else
|
|
{
|
|
char *fname;
|
|
|
|
fname = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
expand_tilde(&fname);
|
|
if (fname)
|
|
canonicalize_path(fname);
|
|
if (do_edit(fname, query_buf, NULL))
|
|
status = PSQL_CMD_NEWEDIT;
|
|
else
|
|
status = PSQL_CMD_ERROR;
|
|
free(fname);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \ef -- edit the named function, or present a blank CREATE FUNCTION
|
|
* template if no argument is given
|
|
*/
|
|
else if (strcmp(cmd, "ef") == 0)
|
|
{
|
|
if (!query_buf)
|
|
{
|
|
psql_error("no query buffer\n");
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
else
|
|
{
|
|
char *func;
|
|
Oid foid = InvalidOid;
|
|
|
|
func = psql_scan_slash_option(scan_state,
|
|
OT_WHOLE_LINE, NULL, true);
|
|
if (!func)
|
|
{
|
|
/* set up an empty command to fill in */
|
|
printfPQExpBuffer(query_buf,
|
|
"CREATE FUNCTION ( )\n"
|
|
" RETURNS \n"
|
|
" LANGUAGE \n"
|
|
" -- common options: IMMUTABLE STABLE STRICT SECURITY DEFINER\n"
|
|
"AS $function$\n"
|
|
"\n$function$\n");
|
|
}
|
|
else if (!lookup_function_oid(pset.db, func, &foid))
|
|
{
|
|
/* error already reported */
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
else if (!get_create_function_cmd(pset.db, foid, query_buf))
|
|
{
|
|
/* error already reported */
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
if (func)
|
|
free(func);
|
|
}
|
|
|
|
if (status != PSQL_CMD_ERROR)
|
|
{
|
|
bool edited = false;
|
|
|
|
if (!do_edit(0, query_buf, &edited))
|
|
status = PSQL_CMD_ERROR;
|
|
else if (!edited)
|
|
puts(_("No changes"));
|
|
else
|
|
status = PSQL_CMD_NEWEDIT;
|
|
}
|
|
}
|
|
|
|
/* \echo and \qecho */
|
|
else if (strcmp(cmd, "echo") == 0 || strcmp(cmd, "qecho") == 0)
|
|
{
|
|
char *value;
|
|
char quoted;
|
|
bool no_newline = false;
|
|
bool first = true;
|
|
FILE *fout;
|
|
|
|
if (strcmp(cmd, "qecho") == 0)
|
|
fout = pset.queryFout;
|
|
else
|
|
fout = stdout;
|
|
|
|
while ((value = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, "ed, false)))
|
|
{
|
|
if (!quoted && strcmp(value, "-n") == 0)
|
|
no_newline = true;
|
|
else
|
|
{
|
|
if (first)
|
|
first = false;
|
|
else
|
|
fputc(' ', fout);
|
|
fputs(value, fout);
|
|
}
|
|
free(value);
|
|
}
|
|
if (!no_newline)
|
|
fputs("\n", fout);
|
|
}
|
|
|
|
/* \encoding -- set/show client side encoding */
|
|
else if (strcmp(cmd, "encoding") == 0)
|
|
{
|
|
char *encoding = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
if (!encoding)
|
|
{
|
|
/* show encoding */
|
|
puts(pg_encoding_to_char(pset.encoding));
|
|
}
|
|
else
|
|
{
|
|
/* set encoding */
|
|
if (PQsetClientEncoding(pset.db, encoding) == -1)
|
|
psql_error("%s: invalid encoding name or conversion procedure not found\n", encoding);
|
|
else
|
|
{
|
|
/* save encoding info into psql internal data */
|
|
pset.encoding = PQclientEncoding(pset.db);
|
|
pset.popt.topt.encoding = pset.encoding;
|
|
SetVariable(pset.vars, "ENCODING",
|
|
pg_encoding_to_char(pset.encoding));
|
|
}
|
|
free(encoding);
|
|
}
|
|
}
|
|
|
|
/* \f -- change field separator */
|
|
else if (strcmp(cmd, "f") == 0)
|
|
{
|
|
char *fname = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
success = do_pset("fieldsep", fname, &pset.popt, pset.quiet);
|
|
free(fname);
|
|
}
|
|
|
|
/* \g means send query */
|
|
else if (strcmp(cmd, "g") == 0)
|
|
{
|
|
char *fname = psql_scan_slash_option(scan_state,
|
|
OT_FILEPIPE, NULL, false);
|
|
|
|
if (!fname)
|
|
pset.gfname = NULL;
|
|
else
|
|
{
|
|
expand_tilde(&fname);
|
|
pset.gfname = pg_strdup(fname);
|
|
}
|
|
free(fname);
|
|
status = PSQL_CMD_SEND;
|
|
}
|
|
|
|
/* help */
|
|
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_WHOLE_LINE, NULL, false);
|
|
size_t len;
|
|
|
|
/* strip any trailing spaces and semicolons */
|
|
if (opt)
|
|
{
|
|
len = strlen(opt);
|
|
while (len > 0 &&
|
|
(isspace((unsigned char) opt[len - 1])
|
|
|| opt[len - 1] == ';'))
|
|
opt[--len] = '\0';
|
|
}
|
|
|
|
helpSQL(opt, pset.popt.topt.pager);
|
|
free(opt);
|
|
}
|
|
|
|
/* HTML mode */
|
|
else if (strcmp(cmd, "H") == 0 || strcmp(cmd, "html") == 0)
|
|
{
|
|
if (pset.popt.topt.format != PRINT_HTML)
|
|
success = do_pset("format", "html", &pset.popt, pset.quiet);
|
|
else
|
|
success = do_pset("format", "aligned", &pset.popt, pset.quiet);
|
|
}
|
|
|
|
|
|
/* \i is include file */
|
|
else if (strcmp(cmd, "i") == 0 || strcmp(cmd, "include") == 0)
|
|
{
|
|
char *fname = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
if (!fname)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
expand_tilde(&fname);
|
|
success = (process_file(fname, false) == EXIT_SUCCESS);
|
|
free(fname);
|
|
}
|
|
}
|
|
|
|
/* \l is list databases */
|
|
else if (strcmp(cmd, "l") == 0 || strcmp(cmd, "list") == 0)
|
|
success = listAllDbs(false);
|
|
else if (strcmp(cmd, "l+") == 0 || strcmp(cmd, "list+") == 0)
|
|
success = listAllDbs(true);
|
|
|
|
/*
|
|
* large object things
|
|
*/
|
|
else if (strncmp(cmd, "lo_", 3) == 0)
|
|
{
|
|
char *opt1,
|
|
*opt2;
|
|
|
|
opt1 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
opt2 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
if (strcmp(cmd + 3, "export") == 0)
|
|
{
|
|
if (!opt2)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
expand_tilde(&opt2);
|
|
success = do_lo_export(opt1, opt2);
|
|
}
|
|
}
|
|
|
|
else if (strcmp(cmd + 3, "import") == 0)
|
|
{
|
|
if (!opt1)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
expand_tilde(&opt1);
|
|
success = do_lo_import(opt1, opt2);
|
|
}
|
|
}
|
|
|
|
else if (strcmp(cmd + 3, "list") == 0)
|
|
success = do_lo_list();
|
|
|
|
else if (strcmp(cmd + 3, "unlink") == 0)
|
|
{
|
|
if (!opt1)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
success = do_lo_unlink(opt1);
|
|
}
|
|
|
|
else
|
|
status = PSQL_CMD_UNKNOWN;
|
|
|
|
free(opt1);
|
|
free(opt2);
|
|
}
|
|
|
|
|
|
/* \o -- set query output */
|
|
else if (strcmp(cmd, "o") == 0 || strcmp(cmd, "out") == 0)
|
|
{
|
|
char *fname = psql_scan_slash_option(scan_state,
|
|
OT_FILEPIPE, NULL, true);
|
|
|
|
expand_tilde(&fname);
|
|
success = setQFout(fname);
|
|
free(fname);
|
|
}
|
|
|
|
/* \p prints the current query buffer */
|
|
else if (strcmp(cmd, "p") == 0 || strcmp(cmd, "print") == 0)
|
|
{
|
|
if (query_buf && query_buf->len > 0)
|
|
puts(query_buf->data);
|
|
else if (!pset.quiet)
|
|
puts(_("Query buffer is empty."));
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* \password -- set user password */
|
|
else if (strcmp(cmd, "password") == 0)
|
|
{
|
|
char *pw1;
|
|
char *pw2;
|
|
|
|
pw1 = simple_prompt("Enter new password: ", 100, false);
|
|
pw2 = simple_prompt("Enter it again: ", 100, false);
|
|
|
|
if (strcmp(pw1, pw2) != 0)
|
|
{
|
|
fprintf(stderr, _("Passwords didn't match.\n"));
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
char *opt0 = psql_scan_slash_option(scan_state, OT_SQLID, NULL, true);
|
|
char *user;
|
|
char *encrypted_password;
|
|
|
|
if (opt0)
|
|
user = opt0;
|
|
else
|
|
user = PQuser(pset.db);
|
|
|
|
encrypted_password = PQencryptPassword(pw1, user);
|
|
|
|
if (!encrypted_password)
|
|
{
|
|
fprintf(stderr, _("Password encryption failed.\n"));
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
PQExpBufferData buf;
|
|
PGresult *res;
|
|
|
|
initPQExpBuffer(&buf);
|
|
printfPQExpBuffer(&buf, "ALTER USER %s PASSWORD ",
|
|
fmtId(user));
|
|
appendStringLiteralConn(&buf, encrypted_password, pset.db);
|
|
res = PSQLexec(buf.data, false);
|
|
termPQExpBuffer(&buf);
|
|
if (!res)
|
|
success = false;
|
|
else
|
|
PQclear(res);
|
|
PQfreemem(encrypted_password);
|
|
}
|
|
}
|
|
|
|
free(pw1);
|
|
free(pw2);
|
|
}
|
|
|
|
/* \prompt -- prompt and set variable */
|
|
else if (strcmp(cmd, "prompt") == 0)
|
|
{
|
|
char *opt,
|
|
*prompt_text = NULL;
|
|
char *arg1,
|
|
*arg2;
|
|
|
|
arg1 = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false);
|
|
arg2 = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false);
|
|
|
|
if (!arg1)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
char *result;
|
|
|
|
if (arg2)
|
|
{
|
|
prompt_text = arg1;
|
|
opt = arg2;
|
|
}
|
|
else
|
|
opt = arg1;
|
|
|
|
if (!pset.inputfile)
|
|
result = simple_prompt(prompt_text, 4096, true);
|
|
else
|
|
{
|
|
if (prompt_text)
|
|
{
|
|
fputs(prompt_text, stdout);
|
|
fflush(stdout);
|
|
}
|
|
result = gets_fromFile(stdin);
|
|
}
|
|
|
|
if (!SetVariable(pset.vars, opt, result))
|
|
{
|
|
psql_error("\\%s: error\n", cmd);
|
|
success = false;
|
|
}
|
|
|
|
free(result);
|
|
if (prompt_text)
|
|
free(prompt_text);
|
|
free(opt);
|
|
}
|
|
}
|
|
|
|
/* \pset -- set printing parameters */
|
|
else if (strcmp(cmd, "pset") == 0)
|
|
{
|
|
char *opt0 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
char *opt1 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
if (!opt0)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
success = do_pset(opt0, opt1, &pset.popt, pset.quiet);
|
|
|
|
free(opt0);
|
|
free(opt1);
|
|
}
|
|
|
|
/* \q or \quit */
|
|
else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0)
|
|
status = PSQL_CMD_TERMINATE;
|
|
|
|
/* reset(clear) the buffer */
|
|
else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0)
|
|
{
|
|
resetPQExpBuffer(query_buf);
|
|
psql_scan_reset(scan_state);
|
|
if (!pset.quiet)
|
|
puts(_("Query buffer reset (cleared)."));
|
|
}
|
|
|
|
/* \s save history in a file or show it on the screen */
|
|
else if (strcmp(cmd, "s") == 0)
|
|
{
|
|
char *fname = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
expand_tilde(&fname);
|
|
/* This scrolls off the screen when using /dev/tty */
|
|
success = saveHistory(fname ? fname : DEVTTY, -1, false, false);
|
|
if (success && !pset.quiet && fname)
|
|
printf(gettext("Wrote history to file \"%s/%s\".\n"),
|
|
pset.dirname ? pset.dirname : ".", fname);
|
|
if (!fname)
|
|
putchar('\n');
|
|
free(fname);
|
|
}
|
|
|
|
/* \set -- generalized set variable/option command */
|
|
else if (strcmp(cmd, "set") == 0)
|
|
{
|
|
char *opt0 = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
if (!opt0)
|
|
{
|
|
/* list all variables */
|
|
PrintVariables(pset.vars);
|
|
success = true;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Set variable to the concatenation of the arguments.
|
|
*/
|
|
char *newval;
|
|
char *opt;
|
|
|
|
opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
newval = pg_strdup(opt ? opt : "");
|
|
free(opt);
|
|
|
|
while ((opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false)))
|
|
{
|
|
newval = realloc(newval, strlen(newval) + strlen(opt) + 1);
|
|
if (!newval)
|
|
{
|
|
psql_error("out of memory\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
strcat(newval, opt);
|
|
free(opt);
|
|
}
|
|
|
|
if (!SetVariable(pset.vars, opt0, newval))
|
|
{
|
|
psql_error("\\%s: error\n", cmd);
|
|
success = false;
|
|
}
|
|
free(newval);
|
|
}
|
|
free(opt0);
|
|
}
|
|
|
|
/* \t -- turn off headers and row count */
|
|
else if (strcmp(cmd, "t") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
success = do_pset("tuples_only", opt, &pset.popt, pset.quiet);
|
|
free(opt);
|
|
}
|
|
|
|
|
|
/* \T -- define html <table ...> attributes */
|
|
else if (strcmp(cmd, "T") == 0)
|
|
{
|
|
char *value = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
success = do_pset("tableattr", value, &pset.popt, pset.quiet);
|
|
free(value);
|
|
}
|
|
|
|
/* \timing -- toggle timing of queries */
|
|
else if (strcmp(cmd, "timing") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
if (opt)
|
|
pset.timing = ParseVariableBool(opt);
|
|
else
|
|
pset.timing = !pset.timing;
|
|
if (!pset.quiet)
|
|
{
|
|
if (pset.timing)
|
|
puts(_("Timing is on."));
|
|
else
|
|
puts(_("Timing is off."));
|
|
}
|
|
free(opt);
|
|
}
|
|
|
|
/* \unset */
|
|
else if (strcmp(cmd, "unset") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, false);
|
|
|
|
if (!opt)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else if (!SetVariable(pset.vars, opt, NULL))
|
|
{
|
|
psql_error("\\%s: error\n", cmd);
|
|
success = false;
|
|
}
|
|
free(opt);
|
|
}
|
|
|
|
/* \w -- write query buffer to file */
|
|
else if (strcmp(cmd, "w") == 0 || strcmp(cmd, "write") == 0)
|
|
{
|
|
FILE *fd = NULL;
|
|
bool is_pipe = false;
|
|
char *fname = NULL;
|
|
|
|
if (!query_buf)
|
|
{
|
|
psql_error("no query buffer\n");
|
|
status = PSQL_CMD_ERROR;
|
|
}
|
|
else
|
|
{
|
|
fname = psql_scan_slash_option(scan_state,
|
|
OT_FILEPIPE, NULL, true);
|
|
expand_tilde(&fname);
|
|
|
|
if (!fname)
|
|
{
|
|
psql_error("\\%s: missing required argument\n", cmd);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
if (fname[0] == '|')
|
|
{
|
|
is_pipe = true;
|
|
fd = popen(&fname[1], "w");
|
|
}
|
|
else
|
|
{
|
|
canonicalize_path(fname);
|
|
fd = fopen(fname, "w");
|
|
}
|
|
if (!fd)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
success = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fd)
|
|
{
|
|
int result;
|
|
|
|
if (query_buf && query_buf->len > 0)
|
|
fprintf(fd, "%s\n", query_buf->data);
|
|
|
|
if (is_pipe)
|
|
result = pclose(fd);
|
|
else
|
|
result = fclose(fd);
|
|
|
|
if (result == EOF)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
free(fname);
|
|
}
|
|
|
|
/* \x -- toggle expanded table representation */
|
|
else if (strcmp(cmd, "x") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
success = do_pset("expanded", opt, &pset.popt, pset.quiet);
|
|
free(opt);
|
|
}
|
|
|
|
/* \z -- list table rights (equivalent to \dp) */
|
|
else if (strcmp(cmd, "z") == 0)
|
|
{
|
|
char *pattern = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true);
|
|
|
|
success = permissionsList(pattern);
|
|
if (pattern)
|
|
free(pattern);
|
|
}
|
|
|
|
/* \! -- shell escape */
|
|
else if (strcmp(cmd, "!") == 0)
|
|
{
|
|
char *opt = psql_scan_slash_option(scan_state,
|
|
OT_WHOLE_LINE, NULL, false);
|
|
|
|
success = do_shell(opt);
|
|
free(opt);
|
|
}
|
|
|
|
/* \? -- slash command help */
|
|
else if (strcmp(cmd, "?") == 0)
|
|
slashUsage(pset.popt.topt.pager);
|
|
|
|
#if 0
|
|
|
|
/*
|
|
* These commands don't do anything. I just use them to test the parser.
|
|
*/
|
|
else if (strcmp(cmd, "void") == 0 || strcmp(cmd, "#") == 0)
|
|
{
|
|
int i = 0;
|
|
char *value;
|
|
|
|
while ((value = psql_scan_slash_option(scan_state,
|
|
OT_NORMAL, NULL, true)))
|
|
{
|
|
fprintf(stderr, "+ opt(%d) = |%s|\n", i++, value);
|
|
free(value);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
else
|
|
status = PSQL_CMD_UNKNOWN;
|
|
|
|
if (!success)
|
|
status = PSQL_CMD_ERROR;
|
|
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
* Ask the user for a password; 'username' is the username the
|
|
* password is for, if one has been explicitly specified. Returns a
|
|
* malloc'd string.
|
|
*/
|
|
static char *
|
|
prompt_for_password(const char *username)
|
|
{
|
|
char *result;
|
|
|
|
if (username == NULL)
|
|
result = simple_prompt("Password: ", 100, false);
|
|
else
|
|
{
|
|
char *prompt_text;
|
|
|
|
prompt_text = malloc(strlen(username) + 100);
|
|
snprintf(prompt_text, strlen(username) + 100,
|
|
_("Password for user %s: "), username);
|
|
result = simple_prompt(prompt_text, 100, false);
|
|
free(prompt_text);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
param_is_newly_set(const char *old_val, const char *new_val)
|
|
{
|
|
if (new_val == NULL)
|
|
return false;
|
|
|
|
if (old_val == NULL || strcmp(old_val, new_val) != 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* do_connect -- handler for \connect
|
|
*
|
|
* Connects to a database with given parameters. If there exists an
|
|
* established connection, NULL values will be replaced with the ones
|
|
* in the current connection. Otherwise NULL will be passed for that
|
|
* parameter to PQconnectdbParams(), so the libpq defaults will be used.
|
|
*
|
|
* In interactive mode, if connection fails with the given parameters,
|
|
* the old connection will be kept.
|
|
*/
|
|
static bool
|
|
do_connect(char *dbname, char *user, char *host, char *port)
|
|
{
|
|
PGconn *o_conn = pset.db,
|
|
*n_conn;
|
|
char *password = NULL;
|
|
|
|
if (!dbname)
|
|
dbname = PQdb(o_conn);
|
|
if (!user)
|
|
user = PQuser(o_conn);
|
|
if (!host)
|
|
host = PQhost(o_conn);
|
|
if (!port)
|
|
port = PQport(o_conn);
|
|
|
|
/*
|
|
* If the user asked to be prompted for a password, ask for one now. If
|
|
* not, use the password from the old connection, provided the username
|
|
* has not changed. Otherwise, try to connect without a password first,
|
|
* and then ask for a password if needed.
|
|
*
|
|
* XXX: this behavior leads to spurious connection attempts recorded in
|
|
* the postmaster's log. But libpq offers no API that would let us obtain
|
|
* a password and then continue with the first connection attempt.
|
|
*/
|
|
if (pset.getPassword == TRI_YES)
|
|
{
|
|
password = prompt_for_password(user);
|
|
}
|
|
else if (o_conn && user && strcmp(PQuser(o_conn), user) == 0)
|
|
{
|
|
password = strdup(PQpass(o_conn));
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
#define PARAMS_ARRAY_SIZE 7
|
|
const char **keywords = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*keywords));
|
|
const char **values = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*values));
|
|
|
|
keywords[0] = "host";
|
|
values[0] = host;
|
|
keywords[1] = "port";
|
|
values[1] = port;
|
|
keywords[2] = "user";
|
|
values[2] = user;
|
|
keywords[3] = "password";
|
|
values[3] = password;
|
|
keywords[4] = "dbname";
|
|
values[4] = dbname;
|
|
keywords[5] = "fallback_application_name";
|
|
values[5] = pset.progname;
|
|
keywords[6] = NULL;
|
|
values[6] = NULL;
|
|
|
|
n_conn = PQconnectdbParams(keywords, values, true);
|
|
|
|
free(keywords);
|
|
free(values);
|
|
|
|
/* We can immediately discard the password -- no longer needed */
|
|
if (password)
|
|
free(password);
|
|
|
|
if (PQstatus(n_conn) == CONNECTION_OK)
|
|
break;
|
|
|
|
/*
|
|
* Connection attempt failed; either retry the connection attempt with
|
|
* a new password, or give up.
|
|
*/
|
|
if (!password && PQconnectionNeedsPassword(n_conn) && pset.getPassword != TRI_NO)
|
|
{
|
|
PQfinish(n_conn);
|
|
password = prompt_for_password(user);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Failed to connect to the database. In interactive mode, keep the
|
|
* previous connection to the DB; in scripting mode, close our
|
|
* previous connection as well.
|
|
*/
|
|
if (pset.cur_cmd_interactive)
|
|
{
|
|
psql_error("%s", PQerrorMessage(n_conn));
|
|
|
|
/* pset.db is left unmodified */
|
|
if (o_conn)
|
|
fputs(_("Previous connection kept\n"), stderr);
|
|
}
|
|
else
|
|
{
|
|
psql_error("\\connect: %s", PQerrorMessage(n_conn));
|
|
if (o_conn)
|
|
{
|
|
PQfinish(o_conn);
|
|
pset.db = NULL;
|
|
}
|
|
}
|
|
|
|
PQfinish(n_conn);
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Replace the old connection with the new one, and update
|
|
* connection-dependent variables.
|
|
*/
|
|
PQsetNoticeProcessor(n_conn, NoticeProcessor, NULL);
|
|
pset.db = n_conn;
|
|
SyncVariables();
|
|
connection_warnings(false); /* Must be after SyncVariables */
|
|
|
|
/* Tell the user about the new connection */
|
|
if (!pset.quiet)
|
|
{
|
|
printf(_("You are now connected to database \"%s\""), PQdb(pset.db));
|
|
|
|
if (param_is_newly_set(PQhost(o_conn), PQhost(pset.db)))
|
|
printf(_(" on host \"%s\""), PQhost(pset.db));
|
|
|
|
if (param_is_newly_set(PQport(o_conn), PQport(pset.db)))
|
|
printf(_(" at port \"%s\""), PQport(pset.db));
|
|
|
|
if (param_is_newly_set(PQuser(o_conn), PQuser(pset.db)))
|
|
printf(_(" as user \"%s\""), PQuser(pset.db));
|
|
|
|
printf(".\n");
|
|
}
|
|
|
|
if (o_conn)
|
|
PQfinish(o_conn);
|
|
return true;
|
|
}
|
|
|
|
|
|
void
|
|
connection_warnings(bool in_startup)
|
|
{
|
|
if (!pset.quiet && !pset.notty)
|
|
{
|
|
int client_ver = parse_version(PG_VERSION);
|
|
|
|
if (pset.sversion != client_ver)
|
|
{
|
|
const char *server_version;
|
|
char server_ver_str[16];
|
|
|
|
/* Try to get full text form, might include "devel" etc */
|
|
server_version = PQparameterStatus(pset.db, "server_version");
|
|
if (!server_version)
|
|
{
|
|
snprintf(server_ver_str, sizeof(server_ver_str),
|
|
"%d.%d.%d",
|
|
pset.sversion / 10000,
|
|
(pset.sversion / 100) % 100,
|
|
pset.sversion % 100);
|
|
server_version = server_ver_str;
|
|
}
|
|
|
|
printf(_("%s (%s, server %s)\n"),
|
|
pset.progname, PG_VERSION, server_version);
|
|
}
|
|
/* For version match, only print psql banner on startup. */
|
|
else if (in_startup)
|
|
printf("%s (%s)\n", pset.progname, PG_VERSION);
|
|
|
|
if (pset.sversion / 100 != client_ver / 100)
|
|
printf(_("WARNING: %s version %d.%d, server version %d.%d.\n"
|
|
" Some psql features might not work.\n"),
|
|
pset.progname, client_ver / 10000, (client_ver / 100) % 100,
|
|
pset.sversion / 10000, (pset.sversion / 100) % 100);
|
|
|
|
#ifdef WIN32
|
|
checkWin32Codepage();
|
|
#endif
|
|
printSSLInfo();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* printSSLInfo
|
|
*
|
|
* Prints information about the current SSL connection, if SSL is in use
|
|
*/
|
|
static void
|
|
printSSLInfo(void)
|
|
{
|
|
#ifdef USE_SSL
|
|
int sslbits = -1;
|
|
SSL *ssl;
|
|
|
|
ssl = PQgetssl(pset.db);
|
|
if (!ssl)
|
|
return; /* no SSL */
|
|
|
|
SSL_get_cipher_bits(ssl, &sslbits);
|
|
printf(_("SSL connection (cipher: %s, bits: %i)\n"),
|
|
SSL_get_cipher(ssl), sslbits);
|
|
#else
|
|
|
|
/*
|
|
* If psql is compiled without SSL but is using a libpq with SSL, we
|
|
* cannot figure out the specifics about the connection. But we know it's
|
|
* SSL secured.
|
|
*/
|
|
if (PQgetssl(pset.db))
|
|
printf(_("SSL connection (unknown cipher)\n"));
|
|
#endif
|
|
}
|
|
|
|
|
|
/*
|
|
* checkWin32Codepage
|
|
*
|
|
* Prints a warning when win32 console codepage differs from Windows codepage
|
|
*/
|
|
#ifdef WIN32
|
|
static void
|
|
checkWin32Codepage(void)
|
|
{
|
|
unsigned int wincp,
|
|
concp;
|
|
|
|
wincp = GetACP();
|
|
concp = GetConsoleCP();
|
|
if (wincp != concp)
|
|
{
|
|
printf(_("WARNING: Console code page (%u) differs from Windows code page (%u)\n"
|
|
" 8-bit characters might not work correctly. See psql reference\n"
|
|
" page \"Notes for Windows users\" for details.\n"),
|
|
concp, wincp);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* SyncVariables
|
|
*
|
|
* Make psql's internal variables agree with connection state upon
|
|
* establishing a new connection.
|
|
*/
|
|
void
|
|
SyncVariables(void)
|
|
{
|
|
/* get stuff from connection */
|
|
pset.encoding = PQclientEncoding(pset.db);
|
|
pset.popt.topt.encoding = pset.encoding;
|
|
pset.sversion = PQserverVersion(pset.db);
|
|
|
|
SetVariable(pset.vars, "DBNAME", PQdb(pset.db));
|
|
SetVariable(pset.vars, "USER", PQuser(pset.db));
|
|
SetVariable(pset.vars, "HOST", PQhost(pset.db));
|
|
SetVariable(pset.vars, "PORT", PQport(pset.db));
|
|
SetVariable(pset.vars, "ENCODING", pg_encoding_to_char(pset.encoding));
|
|
|
|
/* send stuff to it, too */
|
|
PQsetErrorVerbosity(pset.db, pset.verbosity);
|
|
}
|
|
|
|
/*
|
|
* UnsyncVariables
|
|
*
|
|
* Clear variables that should be not be set when there is no connection.
|
|
*/
|
|
void
|
|
UnsyncVariables(void)
|
|
{
|
|
SetVariable(pset.vars, "DBNAME", NULL);
|
|
SetVariable(pset.vars, "USER", NULL);
|
|
SetVariable(pset.vars, "HOST", NULL);
|
|
SetVariable(pset.vars, "PORT", NULL);
|
|
SetVariable(pset.vars, "ENCODING", NULL);
|
|
}
|
|
|
|
|
|
/*
|
|
* do_edit -- handler for \e
|
|
*
|
|
* If you do not specify a filename, the current query buffer will be copied
|
|
* into a temporary one.
|
|
*/
|
|
|
|
static bool
|
|
editFile(const char *fname)
|
|
{
|
|
const char *editorName;
|
|
char *sys;
|
|
int result;
|
|
|
|
psql_assert(fname);
|
|
|
|
/* Find an editor to use */
|
|
editorName = getenv("PSQL_EDITOR");
|
|
if (!editorName)
|
|
editorName = getenv("EDITOR");
|
|
if (!editorName)
|
|
editorName = getenv("VISUAL");
|
|
if (!editorName)
|
|
editorName = DEFAULT_EDITOR;
|
|
|
|
/*
|
|
* On Unix the EDITOR value should *not* be quoted, since it might include
|
|
* switches, eg, EDITOR="pico -t"; it's up to the user to put quotes in it
|
|
* if necessary. But this policy is not very workable on Windows, due to
|
|
* severe brain damage in their command shell plus the fact that standard
|
|
* program paths include spaces.
|
|
*/
|
|
sys = pg_malloc(strlen(editorName) + strlen(fname) + 10 + 1);
|
|
#ifndef WIN32
|
|
sprintf(sys, "exec %s '%s'", editorName, fname);
|
|
#else
|
|
sprintf(sys, SYSTEMQUOTE "\"%s\" \"%s\"" SYSTEMQUOTE, editorName, fname);
|
|
#endif
|
|
result = system(sys);
|
|
if (result == -1)
|
|
psql_error("could not start editor \"%s\"\n", editorName);
|
|
else if (result == 127)
|
|
psql_error("could not start /bin/sh\n");
|
|
free(sys);
|
|
|
|
return result == 0;
|
|
}
|
|
|
|
|
|
/* call this one */
|
|
static bool
|
|
do_edit(const char *filename_arg, PQExpBuffer query_buf, bool *edited)
|
|
{
|
|
char fnametmp[MAXPGPATH];
|
|
FILE *stream = NULL;
|
|
const char *fname;
|
|
bool error = false;
|
|
int fd;
|
|
|
|
struct stat before,
|
|
after;
|
|
|
|
if (filename_arg)
|
|
fname = filename_arg;
|
|
else
|
|
{
|
|
/* make a temp file to edit */
|
|
#ifndef WIN32
|
|
const char *tmpdir = getenv("TMPDIR");
|
|
|
|
if (!tmpdir)
|
|
tmpdir = "/tmp";
|
|
#else
|
|
char tmpdir[MAXPGPATH];
|
|
int ret;
|
|
|
|
ret = GetTempPath(MAXPGPATH, tmpdir);
|
|
if (ret == 0 || ret > MAXPGPATH)
|
|
{
|
|
psql_error("cannot locate temporary directory: %s",
|
|
!ret ? strerror(errno) : "");
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* No canonicalize_path() here. EDIT.EXE run from CMD.EXE prepends the
|
|
* current directory to the supplied path unless we use only
|
|
* backslashes, so we do that.
|
|
*/
|
|
#endif
|
|
#ifndef WIN32
|
|
snprintf(fnametmp, sizeof(fnametmp), "%s%spsql.edit.%d", tmpdir,
|
|
"/", (int) getpid());
|
|
#else
|
|
snprintf(fnametmp, sizeof(fnametmp), "%s%spsql.edit.%d", tmpdir,
|
|
"" /* trailing separator already present */ , (int) getpid());
|
|
#endif
|
|
|
|
fname = (const char *) fnametmp;
|
|
|
|
fd = open(fname, O_WRONLY | O_CREAT | O_EXCL, 0600);
|
|
if (fd != -1)
|
|
stream = fdopen(fd, "w");
|
|
|
|
if (fd == -1 || !stream)
|
|
{
|
|
psql_error("could not open temporary file \"%s\": %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
else
|
|
{
|
|
unsigned int ql = query_buf->len;
|
|
|
|
if (ql == 0 || query_buf->data[ql - 1] != '\n')
|
|
{
|
|
appendPQExpBufferChar(query_buf, '\n');
|
|
ql++;
|
|
}
|
|
|
|
if (fwrite(query_buf->data, 1, ql, stream) != ql)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
fclose(stream);
|
|
remove(fname);
|
|
error = true;
|
|
}
|
|
else if (fclose(stream) != 0)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
remove(fname);
|
|
error = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!error && stat(fname, &before) != 0)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
|
|
/* call editor */
|
|
if (!error)
|
|
error = !editFile(fname);
|
|
|
|
if (!error && stat(fname, &after) != 0)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
|
|
if (!error && before.st_mtime != after.st_mtime)
|
|
{
|
|
stream = fopen(fname, PG_BINARY_R);
|
|
if (!stream)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
else
|
|
{
|
|
/* read file back into query_buf */
|
|
char line[1024];
|
|
|
|
resetPQExpBuffer(query_buf);
|
|
while (fgets(line, sizeof(line), stream) != NULL)
|
|
appendPQExpBufferStr(query_buf, line);
|
|
|
|
if (ferror(stream))
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
else if (edited)
|
|
{
|
|
*edited = true;
|
|
}
|
|
|
|
fclose(stream);
|
|
}
|
|
}
|
|
|
|
/* remove temp file */
|
|
if (!filename_arg)
|
|
{
|
|
if (remove(fname) == -1)
|
|
{
|
|
psql_error("%s: %s\n", fname, strerror(errno));
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
return !error;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* process_file
|
|
*
|
|
* Read commands from filename and then them to the main processing loop
|
|
* Handler for \i, but can be used for other things as well. Returns
|
|
* MainLoop() error code.
|
|
*/
|
|
int
|
|
process_file(char *filename, bool single_txn)
|
|
{
|
|
FILE *fd;
|
|
int result;
|
|
char *oldfilename;
|
|
PGresult *res;
|
|
|
|
if (!filename)
|
|
return EXIT_FAILURE;
|
|
|
|
if (strcmp(filename, "-") != 0)
|
|
{
|
|
canonicalize_path(filename);
|
|
fd = fopen(filename, PG_BINARY_R);
|
|
}
|
|
else
|
|
fd = stdin;
|
|
|
|
if (!fd)
|
|
{
|
|
psql_error("%s: %s\n", filename, strerror(errno));
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
oldfilename = pset.inputfile;
|
|
pset.inputfile = filename;
|
|
|
|
if (single_txn)
|
|
{
|
|
if ((res = PSQLexec("BEGIN", false)) == NULL)
|
|
{
|
|
if (pset.on_error_stop)
|
|
return EXIT_USER;
|
|
}
|
|
else
|
|
PQclear(res);
|
|
}
|
|
|
|
result = MainLoop(fd);
|
|
|
|
if (single_txn)
|
|
{
|
|
if ((res = PSQLexec("COMMIT", false)) == NULL)
|
|
{
|
|
if (pset.on_error_stop)
|
|
return EXIT_USER;
|
|
}
|
|
else
|
|
PQclear(res);
|
|
}
|
|
|
|
fclose(fd);
|
|
pset.inputfile = oldfilename;
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* do_pset
|
|
*
|
|
*/
|
|
static const char *
|
|
_align2string(enum printFormat in)
|
|
{
|
|
switch (in)
|
|
{
|
|
case PRINT_NOTHING:
|
|
return "nothing";
|
|
break;
|
|
case PRINT_UNALIGNED:
|
|
return "unaligned";
|
|
break;
|
|
case PRINT_ALIGNED:
|
|
return "aligned";
|
|
break;
|
|
case PRINT_WRAPPED:
|
|
return "wrapped";
|
|
break;
|
|
case PRINT_HTML:
|
|
return "html";
|
|
break;
|
|
case PRINT_LATEX:
|
|
return "latex";
|
|
break;
|
|
case PRINT_TROFF_MS:
|
|
return "troff-ms";
|
|
break;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
|
|
bool
|
|
do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
|
|
{
|
|
size_t vallen = 0;
|
|
|
|
psql_assert(param);
|
|
|
|
if (value)
|
|
vallen = strlen(value);
|
|
|
|
/* set format */
|
|
if (strcmp(param, "format") == 0)
|
|
{
|
|
if (!value)
|
|
;
|
|
else if (pg_strncasecmp("unaligned", value, vallen) == 0)
|
|
popt->topt.format = PRINT_UNALIGNED;
|
|
else if (pg_strncasecmp("aligned", value, vallen) == 0)
|
|
popt->topt.format = PRINT_ALIGNED;
|
|
else if (pg_strncasecmp("wrapped", value, vallen) == 0)
|
|
popt->topt.format = PRINT_WRAPPED;
|
|
else if (pg_strncasecmp("html", value, vallen) == 0)
|
|
popt->topt.format = PRINT_HTML;
|
|
else if (pg_strncasecmp("latex", value, vallen) == 0)
|
|
popt->topt.format = PRINT_LATEX;
|
|
else if (pg_strncasecmp("troff-ms", value, vallen) == 0)
|
|
popt->topt.format = PRINT_TROFF_MS;
|
|
else
|
|
{
|
|
psql_error("\\pset: allowed formats are unaligned, aligned, wrapped, html, latex, troff-ms\n");
|
|
return false;
|
|
}
|
|
|
|
if (!quiet)
|
|
printf(_("Output format is %s.\n"), _align2string(popt->topt.format));
|
|
}
|
|
|
|
/* set table line style */
|
|
else if (strcmp(param, "linestyle") == 0)
|
|
{
|
|
if (!value)
|
|
;
|
|
else if (pg_strncasecmp("ascii", value, vallen) == 0)
|
|
popt->topt.line_style = &pg_asciiformat;
|
|
else if (pg_strncasecmp("old-ascii", value, vallen) == 0)
|
|
popt->topt.line_style = &pg_asciiformat_old;
|
|
else if (pg_strncasecmp("unicode", value, vallen) == 0)
|
|
popt->topt.line_style = &pg_utf8format;
|
|
else
|
|
{
|
|
psql_error("\\pset: allowed line styles are ascii, old-ascii, unicode\n");
|
|
return false;
|
|
}
|
|
|
|
if (!quiet)
|
|
printf(_("Line style is %s.\n"),
|
|
get_line_style(&popt->topt)->name);
|
|
}
|
|
|
|
/* set border style/width */
|
|
else if (strcmp(param, "border") == 0)
|
|
{
|
|
if (value)
|
|
popt->topt.border = atoi(value);
|
|
|
|
if (!quiet)
|
|
printf(_("Border style is %d.\n"), popt->topt.border);
|
|
}
|
|
|
|
/* set expanded/vertical mode */
|
|
else if (strcmp(param, "x") == 0 || strcmp(param, "expanded") == 0 || strcmp(param, "vertical") == 0)
|
|
{
|
|
if (value)
|
|
popt->topt.expanded = ParseVariableBool(value);
|
|
else
|
|
popt->topt.expanded = !popt->topt.expanded;
|
|
if (!quiet)
|
|
printf(popt->topt.expanded
|
|
? _("Expanded display is on.\n")
|
|
: _("Expanded display is off.\n"));
|
|
}
|
|
|
|
/* locale-aware numeric output */
|
|
else if (strcmp(param, "numericlocale") == 0)
|
|
{
|
|
if (value)
|
|
popt->topt.numericLocale = ParseVariableBool(value);
|
|
else
|
|
popt->topt.numericLocale = !popt->topt.numericLocale;
|
|
if (!quiet)
|
|
{
|
|
if (popt->topt.numericLocale)
|
|
puts(_("Showing locale-adjusted numeric output."));
|
|
else
|
|
puts(_("Locale-adjusted numeric output is off."));
|
|
}
|
|
}
|
|
|
|
/* null display */
|
|
else if (strcmp(param, "null") == 0)
|
|
{
|
|
if (value)
|
|
{
|
|
free(popt->nullPrint);
|
|
popt->nullPrint = pg_strdup(value);
|
|
}
|
|
if (!quiet)
|
|
printf(_("Null display is \"%s\".\n"), popt->nullPrint ? popt->nullPrint : "");
|
|
}
|
|
|
|
/* field separator for unaligned text */
|
|
else if (strcmp(param, "fieldsep") == 0)
|
|
{
|
|
if (value)
|
|
{
|
|
free(popt->topt.fieldSep);
|
|
popt->topt.fieldSep = pg_strdup(value);
|
|
}
|
|
if (!quiet)
|
|
printf(_("Field separator is \"%s\".\n"), popt->topt.fieldSep);
|
|
}
|
|
|
|
/* record separator for unaligned text */
|
|
else if (strcmp(param, "recordsep") == 0)
|
|
{
|
|
if (value)
|
|
{
|
|
free(popt->topt.recordSep);
|
|
popt->topt.recordSep = pg_strdup(value);
|
|
}
|
|
if (!quiet)
|
|
{
|
|
if (strcmp(popt->topt.recordSep, "\n") == 0)
|
|
printf(_("Record separator is <newline>."));
|
|
else
|
|
printf(_("Record separator is \"%s\".\n"), popt->topt.recordSep);
|
|
}
|
|
}
|
|
|
|
/* toggle between full and tuples-only format */
|
|
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
|
|
{
|
|
if (value)
|
|
popt->topt.tuples_only = ParseVariableBool(value);
|
|
else
|
|
popt->topt.tuples_only = !popt->topt.tuples_only;
|
|
if (!quiet)
|
|
{
|
|
if (popt->topt.tuples_only)
|
|
puts(_("Showing only tuples."));
|
|
else
|
|
puts(_("Tuples only is off."));
|
|
}
|
|
}
|
|
|
|
/* set title override */
|
|
else if (strcmp(param, "title") == 0)
|
|
{
|
|
free(popt->title);
|
|
if (!value)
|
|
popt->title = NULL;
|
|
else
|
|
popt->title = pg_strdup(value);
|
|
|
|
if (!quiet)
|
|
{
|
|
if (popt->title)
|
|
printf(_("Title is \"%s\".\n"), popt->title);
|
|
else
|
|
printf(_("Title is unset.\n"));
|
|
}
|
|
}
|
|
|
|
/* set HTML table tag options */
|
|
else if (strcmp(param, "T") == 0 || strcmp(param, "tableattr") == 0)
|
|
{
|
|
free(popt->topt.tableAttr);
|
|
if (!value)
|
|
popt->topt.tableAttr = NULL;
|
|
else
|
|
popt->topt.tableAttr = pg_strdup(value);
|
|
|
|
if (!quiet)
|
|
{
|
|
if (popt->topt.tableAttr)
|
|
printf(_("Table attribute is \"%s\".\n"), popt->topt.tableAttr);
|
|
else
|
|
printf(_("Table attributes unset.\n"));
|
|
}
|
|
}
|
|
|
|
/* toggle use of pager */
|
|
else if (strcmp(param, "pager") == 0)
|
|
{
|
|
if (value && pg_strcasecmp(value, "always") == 0)
|
|
popt->topt.pager = 2;
|
|
else if (value)
|
|
if (ParseVariableBool(value))
|
|
popt->topt.pager = 1;
|
|
else
|
|
popt->topt.pager = 0;
|
|
else if (popt->topt.pager == 1)
|
|
popt->topt.pager = 0;
|
|
else
|
|
popt->topt.pager = 1;
|
|
if (!quiet)
|
|
{
|
|
if (popt->topt.pager == 1)
|
|
puts(_("Pager is used for long output."));
|
|
else if (popt->topt.pager == 2)
|
|
puts(_("Pager is always used."));
|
|
else
|
|
puts(_("Pager usage is off."));
|
|
}
|
|
}
|
|
|
|
/* disable "(x rows)" footer */
|
|
else if (strcmp(param, "footer") == 0)
|
|
{
|
|
if (value)
|
|
popt->default_footer = ParseVariableBool(value);
|
|
else
|
|
popt->default_footer = !popt->default_footer;
|
|
if (!quiet)
|
|
{
|
|
if (popt->default_footer)
|
|
puts(_("Default footer is on."));
|
|
else
|
|
puts(_("Default footer is off."));
|
|
}
|
|
}
|
|
|
|
/* set border style/width */
|
|
else if (strcmp(param, "columns") == 0)
|
|
{
|
|
if (value)
|
|
popt->topt.columns = atoi(value);
|
|
|
|
if (!quiet)
|
|
printf(_("Target width for \"wrapped\" format is %d.\n"), popt->topt.columns);
|
|
}
|
|
|
|
else
|
|
{
|
|
psql_error("\\pset: unknown option: %s\n", param);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
#ifndef WIN32
|
|
#define DEFAULT_SHELL "/bin/sh"
|
|
#else
|
|
/*
|
|
* CMD.EXE is in different places in different Win32 releases so we
|
|
* have to rely on the path to find it.
|
|
*/
|
|
#define DEFAULT_SHELL "cmd.exe"
|
|
#endif
|
|
|
|
static bool
|
|
do_shell(const char *command)
|
|
{
|
|
int result;
|
|
|
|
if (!command)
|
|
{
|
|
char *sys;
|
|
const char *shellName;
|
|
|
|
shellName = getenv("SHELL");
|
|
#ifdef WIN32
|
|
if (shellName == NULL)
|
|
shellName = getenv("COMSPEC");
|
|
#endif
|
|
if (shellName == NULL)
|
|
shellName = DEFAULT_SHELL;
|
|
|
|
sys = pg_malloc(strlen(shellName) + 16);
|
|
#ifndef WIN32
|
|
sprintf(sys,
|
|
/* See EDITOR handling comment for an explaination */
|
|
"exec %s", shellName);
|
|
#else
|
|
/* See EDITOR handling comment for an explaination */
|
|
sprintf(sys, SYSTEMQUOTE "\"%s\"" SYSTEMQUOTE, shellName);
|
|
#endif
|
|
result = system(sys);
|
|
free(sys);
|
|
}
|
|
else
|
|
result = system(command);
|
|
|
|
if (result == 127 || result == -1)
|
|
{
|
|
psql_error("\\!: failed\n");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* This function takes a function description, e.g. "x" or "x(int)", and
|
|
* issues a query on the given connection to retrieve the function's OID
|
|
* using a cast to regproc or regprocedure (as appropriate). The result,
|
|
* if there is one, is returned at *foid. Note that we'll fail if the
|
|
* function doesn't exist OR if there are multiple matching candidates
|
|
* OR if there's something syntactically wrong with the function description;
|
|
* unfortunately it can be hard to tell the difference.
|
|
*/
|
|
static bool
|
|
lookup_function_oid(PGconn *conn, const char *desc, Oid *foid)
|
|
{
|
|
bool result = true;
|
|
PQExpBuffer query;
|
|
PGresult *res;
|
|
|
|
query = createPQExpBuffer();
|
|
printfPQExpBuffer(query, "SELECT ");
|
|
appendStringLiteralConn(query, desc, conn);
|
|
appendPQExpBuffer(query, "::pg_catalog.%s::pg_catalog.oid",
|
|
strchr(desc, '(') ? "regprocedure" : "regproc");
|
|
|
|
res = PQexec(conn, query->data);
|
|
if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) == 1)
|
|
*foid = atooid(PQgetvalue(res, 0, 0));
|
|
else
|
|
{
|
|
minimal_error_message(res);
|
|
result = false;
|
|
}
|
|
|
|
PQclear(res);
|
|
destroyPQExpBuffer(query);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Fetches the "CREATE OR REPLACE FUNCTION ..." command that describes the
|
|
* function with the given OID. If successful, the result is stored in buf.
|
|
*/
|
|
static bool
|
|
get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf)
|
|
{
|
|
bool result = true;
|
|
PQExpBuffer query;
|
|
PGresult *res;
|
|
|
|
query = createPQExpBuffer();
|
|
printfPQExpBuffer(query, "SELECT pg_catalog.pg_get_functiondef(%u)", oid);
|
|
|
|
res = PQexec(conn, query->data);
|
|
if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) == 1)
|
|
{
|
|
resetPQExpBuffer(buf);
|
|
appendPQExpBufferStr(buf, PQgetvalue(res, 0, 0));
|
|
}
|
|
else
|
|
{
|
|
minimal_error_message(res);
|
|
result = false;
|
|
}
|
|
|
|
PQclear(res);
|
|
destroyPQExpBuffer(query);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Report just the primary error; this is to avoid cluttering the output
|
|
* with, for instance, a redisplay of the internally generated query
|
|
*/
|
|
static void
|
|
minimal_error_message(PGresult *res)
|
|
{
|
|
PQExpBuffer msg;
|
|
const char *fld;
|
|
|
|
msg = createPQExpBuffer();
|
|
|
|
fld = PQresultErrorField(res, PG_DIAG_SEVERITY);
|
|
if (fld)
|
|
printfPQExpBuffer(msg, "%s: ", fld);
|
|
else
|
|
printfPQExpBuffer(msg, "ERROR: ");
|
|
fld = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY);
|
|
if (fld)
|
|
appendPQExpBufferStr(msg, fld);
|
|
else
|
|
appendPQExpBufferStr(msg, "(not available)");
|
|
appendPQExpBufferStr(msg, "\n");
|
|
|
|
psql_error("%s", msg->data);
|
|
|
|
destroyPQExpBuffer(msg);
|
|
}
|