*** a/src/bin/psql/Makefile --- b/src/bin/psql/Makefile *************** *** 20,26 **** REFDOCDIR= $(top_srcdir)/doc/src/sgml/ref override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/pg_dump $(CPPFLAGS) ! OBJS= command.o common.o help.o input.o stringutils.o mainloop.o copy.o \ startup.o prompt.o variables.o large_obj.o print.o describe.o \ tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \ sql_help.o \ --- 20,26 ---- override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/pg_dump $(CPPFLAGS) ! OBJS= ans.o command.o common.o help.o input.o stringutils.o mainloop.o copy.o \ startup.o prompt.o variables.o large_obj.o print.o describe.o \ tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \ sql_help.o \ *** /dev/null --- b/src/bin/psql/ans.c *************** *** 0 **** --- 1,462 ---- + /* + * psql - the PostgreSQL interactive terminal + * + * Copyright (c) 2013, PostgreSQL Global Development Group + * + * src/bin/psql/ans.c + */ + + #include "ans.h" + + #include "postgres_fe.h" + + static int global_ans_num = 0; + + static void CreateTable(PGconn *db, struct _ans* item); + static char* GetTypeName(PGconn *db, Oid oid); + static char* BuildData(PGresult* result); + + /* + * Creates empty entry, serving as list head + */ + AnsHistory CreateAnsHistory(void) + { + struct _ans* head; + + head = pg_malloc(sizeof(struct _ans)); + + head->numColumns = 0; + head->columnTypes = NULL; + head->columnNames = NULL; + head->data = NULL; + head->next = NULL; + head->tableName = NULL; + head->name = NULL; + + return head; + } + + void + AddToHistory(AnsHistory history, PGresult* result) + { + struct _ans* item; + int numRows, numColumns; + int i; + char* data; + + if (!history || !result) + return; + + numColumns = PQnfields(result); + numRows = PQntuples(result); + + if (numColumns <= 0 || numRows <= 0) + return; + + data = BuildData(result); + if (!data) + return; + + item = pg_malloc(sizeof(struct _ans)); + + /* read meta-data: column names and types */ + item->numColumns = numColumns; + item->columnNames = pg_malloc(sizeof(char*) * item->numColumns); + item->columnTypes = pg_malloc(sizeof(Oid) * item->numColumns); + + for (i = 0; i < item->numColumns; i++) + { + char* name; + + item->columnTypes[i] = PQftype(result, i); + + name = PQfname(result, i); + item->columnNames[i] = pg_malloc(strlen(name) + 1); + strcpy(item->columnNames[i], name); + } + + item->data = data; + + /* Table creation deferred to when the ASN is actually used */ + item->tableName = NULL; + + item->name = pg_malloc(10); + sprintf(item->name, "ans%d", global_ans_num++); + + printf(_("Query result stored as :%s\n"), item->name); + + item->next = history->next; + history->next = item; + } + + const char* + GetOrCreateTable(AnsHistory history, PGconn *db, const char* name) + { + struct _ans* item; + + if (!history || !name) + { + return NULL; + } + + item = history->next; + + while (item) + { + if (strcmp(item->name, name) == 0) + { + if (item->tableName) + return item->tableName; + + CreateTable(db, item); + if (item->tableName) + return item->tableName; + else + return NULL; + } + item = item->next; + } + + return NULL; + } + + static + void + CreateTable(PGconn *db, struct _ans* item) + { + char tableNameBuf[32]; + char copyQuery[64]; + const int qbufsize=2048; + char queryBuf[qbufsize]; + char* queryPtr; + int len; + int i; + PGresult* qres; + + if (!item) + return; + + len = snprintf(tableNameBuf, 32, "_table_%s", item->name); + if (len >= 32) + { + return; + } + len = snprintf(copyQuery, 64, "COPY %s FROM STDIN", tableNameBuf); + if (len >= 64) + { + return; + } + + /* Build CREATE TABLE query */ + queryPtr = queryBuf; + queryPtr += snprintf(queryPtr, qbufsize-(int)(queryPtr-queryBuf), "CREATE TEMP TABLE %s (\n", tableNameBuf); + + for (i = 0; i < item->numColumns; ++i) + { + char* typeName; + const char* format; + + typeName = GetTypeName(db, item->columnTypes[i]); + if (!typeName) + return; + + if (i == item->numColumns-1) + format = "%s %s\n"; + else + format = "%s %s,\n"; + + queryPtr += snprintf(queryPtr, qbufsize-(int)(queryPtr-queryBuf), format, item->columnNames[i], typeName); + free(typeName); + if (queryPtr >= queryBuf+qbufsize) + { + return; + } + } + queryPtr += snprintf(queryPtr, qbufsize-(int)(queryPtr-queryBuf), ");"); + if (queryPtr >= queryBuf+qbufsize) + { + return; + } + + /* create and populate table */ + PQclear(PQexec(db, "BEGIN")); + qres = PQexec(db, queryBuf); + if (PQresultStatus(qres) != PGRES_COMMAND_OK) + { + PQclear(qres); + PQclear(PQexec(db, "ROLLBACK")); + return; + } + PQclear(qres); + + qres = PQexec(db, copyQuery); + if (PQresultStatus(qres) != PGRES_COPY_IN) + { + PQclear(qres); + PQclear(PQexec(db, "ROLLBACK")); + return; + } + PQclear(qres); + + PQputCopyData(db, item->data, strlen(item->data)); + len = PQputCopyEnd(db, NULL); + if (len != 1) + { + PQclear(PQexec(db, "ROLLBACK")); + return; + } + qres = PQgetResult(db); + if (PQresultStatus(qres) != PGRES_COMMAND_OK) + { + PQclear(qres); + PQclear(PQexec(db, "ROLLBACK")); + return; + } + PQclear(qres); + PQclear(PQexec(db, "COMMIT")); + + item->tableName = pg_strdup(tableNameBuf); + } + + /* free the typename after use */ + static + char* + GetTypeName(PGconn *db, Oid oid) + { + const int bufsize = 1024; + char queryBuf[bufsize]; + int sres; + PGresult* qres; + char* typeName; + + sres = snprintf(queryBuf, bufsize, "SELECT typname FROM pg_type WHERE oid=%d;", oid); + if ( sres < 0 || sres > bufsize) + return NULL; + + qres = PQexec(db, queryBuf); + if (!qres) + return NULL; + + if (PQresultStatus(qres) != PGRES_TUPLES_OK) + { + PQclear(qres); + return NULL; + } + + if (PQntuples(qres) != 1 && PQnfields(qres) != 1) + { + PQclear(qres); + return NULL; + } + + typeName = pg_strdup(PQgetvalue(qres, 0, 0)); + PQclear(qres); + return typeName; + } + + /* Returns length of string after escaping */ + static + int GetEscapedLen(const char* c) + { + int len = 0; + for(; *c; c++) + { + switch(*c) + { + case '\\': + case '\b': + case '\f': + case '\r': + case '\n': + case '\t': + case '\v': + len++; + default: + len++; + } + } + + return len; + } + + /* Copies c to dst, escaping. Returns pointer past the last character copied. + * Assumes there is enough room in the dst buffer + */ + static + char* + EscapeForCopy(char* dst, const char* c) + { + for(; *c; c++) + { + switch(*c) + { + case '\\': + *(dst++) = '\\'; + *(dst++) = '\\'; + break; + + case '\b': + *(dst++) = '\\'; + *(dst++) = 'b'; + break; + + case '\f': + *(dst++) = '\\'; + *(dst++) = 'f'; + break; + + case '\r': + *(dst++) = '\\'; + *(dst++) = 'r'; + break; + + case '\n': + *(dst++) = '\\'; + *(dst++) = 'n'; + break; + + case '\t': + *(dst++) = '\\'; + *(dst++) = 't'; + break; + + case '\v': + *(dst++) = '\\'; + *(dst++) = 'v'; + break; + + default: + *(dst++) = *c; + } + } + return dst; + } + + /* Convert query data into data buffer in COPY TEXT format. + * returns NULL on error + * caller owns the data + */ + static + char* + BuildData(PGresult* result) + { + int cols, rows; + int bufferSize; + int r,c; + char* buffer; + char* dataPtr; + + if (!result) + return NULL; + + cols = PQnfields(result); + rows = PQntuples(result); + + /* first pass - measure the size */ + bufferSize = 0; + for(r = 0; r < rows; r++) + { + for(c = 0; c < cols; c++) + { + if (PQgetisnull(result, r, c)) + { + bufferSize += 2; /* 2 = size of null literal \N */ + } + else + { + bufferSize += GetEscapedLen(PQgetvalue(result, r, c)); + } + bufferSize +=1; /* column or row delimiter*/ + } + } + bufferSize += 4; /* end-of-data marker \. + NULL terminiator */ + + /* second pass = build the buffer */ + buffer = pg_malloc(bufferSize); + dataPtr = buffer; + + for(r = 0; r < rows; r++) + { + for(c = 0; c < cols; c++) + { + if (PQgetisnull(result, r, c)) + { + strcpy(dataPtr, "\\N"); + dataPtr += 2; + } + else + { + char* value = PQgetvalue(result, r, c); + dataPtr = EscapeForCopy(dataPtr, value); + } + if (c == cols-1) + { + strcpy(dataPtr, "\n"); + } + else + { + strcpy(dataPtr, "\t"); + } + dataPtr += 1; + } + } + strcpy(dataPtr, "\\.\n"); + + return buffer; + } + + void AnsClearTableNames(AnsHistory history) + { + struct _ans* item = history; + + while(item->next) + { + item = item->next; + + if (item->tableName) + { + free(item->tableName); + item->tableName = NULL; + } + } + + } + + void + DestroyAnsHistory(PGconn *db, AnsHistory item) + { + if (!item) + return; + + if (item->next) + DestroyAnsHistory(db, item->next); + + if (item->data) + free(item->data); + + if(item->name) + free(item->name); + + if (item->columnTypes) + free(item->columnTypes); + + if (item->numColumns && item->columnNames) + { + int i; + for(i = 0; i < item->numColumns; i++) + free(item->columnNames[i]); + + free(item->columnNames); + } + + if (item->tableName) + { + const int bufsize = 32; + char deleteQuery[bufsize]; + + snprintf(deleteQuery, bufsize, "DROP TABLE %s", item->tableName); + PQclear(PQexec(db, deleteQuery)); + } + + free(item); + } *** /dev/null --- b/src/bin/psql/ans.h *************** *** 0 **** --- 1,48 ---- + /* + * psql - the PostgreSQL interactive terminal + * + * Copyright (c) 2013, PostgreSQL Global Development Group + * + * src/bin/psql/ans.h + */ + #ifndef ANS_H + #define ANS_H + + #include "libpq-fe.h" + + + /* This forms a list of the last N query results. The format allows us to insert into a temporary table*/ + struct _ans + { + int numColumns; + Oid *columnTypes; + char **columnNames; + char *data; + + char *name; + char *tableName; // corresponding table in the DB. NULL if not created yet + + struct _ans *next; + }; + + typedef struct _ans* AnsHistory; + + AnsHistory CreateAnsHistory(void); + void AddToHistory(AnsHistory history, PGresult* result); + void DestroyAnsHistory(PGconn *db, AnsHistory item); + + + /* + * Checks if the provided name exists in the history. + * If none found, returns NULL. + * If found, but table is not created yet, it creates the table first. + */ + const char* GetOrCreateTable(AnsHistory history, PGconn *db, const char* name); + + /* Should be called when a new connection is opened. + * All the ANS tables are temporary. This means they must be cleared if the client connects + * to a new database. + */ + void AnsClearTableNames(AnsHistory history); + + #endif *** a/src/bin/psql/command.c --- b/src/bin/psql/command.c *************** *** 40,45 **** --- 40,46 ---- #include "pqexpbuffer.h" #include "dumputils.h" + #include "ans.h" #include "common.h" #include "copy.h" #include "describe.h" *************** *** 51,56 **** --- 52,58 ---- #include "psqlscan.h" #include "settings.h" #include "variables.h" + #include "ans.h" /* functions for use in this file */ *************** *** 205,210 **** exec_command(const char *cmd, --- 207,247 ---- success = do_pset("format", "unaligned", &pset.popt, pset.quiet); } + /* \ans - toggle query result history */ + else if (strcmp(cmd, "ans") == 0) + { + char *opt = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + + if (opt) + pset.ans_enabled = ParseVariableBool(opt); + else + // convenience: toggle the setting if the user did not provide an option + pset.ans_enabled = !pset.ans_enabled; + + if (!pset.quiet) + { + if (pset.ans_enabled) + puts(_("Query result history is on.")); + else + { + puts(_("Query result history is off.")); + } + } + free(opt); + } + /* \ansclean - clear query result history */ + else if (strcmp(cmd, "ansclean") == 0) + { + DestroyAnsHistory(pset.db, pset.ans); + pset.ans = CreateAnsHistory(); + + if (!pset.quiet) + { + puts(_("Query result history cleaned.")); + } + } + /* \C -- override table title (formerly change HTML caption) */ else if (strcmp(cmd, "C") == 0) { *************** *** 1697,1702 **** do_connect(char *dbname, char *user, char *host, char *port) --- 1734,1740 ---- PQsetNoticeProcessor(n_conn, NoticeProcessor, NULL); pset.db = n_conn; SyncVariables(); + AnsClearTableNames(pset.ans); connection_warnings(false); /* Must be after SyncVariables */ /* Tell the user about the new connection */ *** a/src/bin/psql/common.c --- b/src/bin/psql/common.c *************** *** 23,28 **** --- 23,29 ---- #include "command.h" #include "copy.h" #include "mbprint.h" + #include "ans.h" *************** *** 950,956 **** SendQuery(const char *query) --- 951,961 ---- /* but printing results isn't: */ if (OK && results) + { OK = PrintQueryResults(results); + if (pset.ans_enabled) + AddToHistory(pset.ans, results); + } } else { *************** *** 1227,1233 **** ExecQueryUsingCursor(const char *query, double *elapsed_msec) } printQuery(results, &my_popt, pset.queryFout, pset.logfile); ! PQclear(results); /* after the first result set, disallow header decoration */ --- 1232,1238 ---- } printQuery(results, &my_popt, pset.queryFout, pset.logfile); ! PQclear(results); /* after the first result set, disallow header decoration */ *** a/src/bin/psql/help.c --- b/src/bin/psql/help.c *************** *** 289,294 **** slashUsage(unsigned short int pager) --- 289,299 ---- " \\lo_import FILE [COMMENT]\n" " \\lo_list\n" " \\lo_unlink LOBOID large object operations\n")); + fprintf(output, "\n"); + + fprintf(output, _("Query result history\n")); + fprintf(output, _(" \\ans [on|off] toggle query result history (currently %s)\n"), ON(pset.ans_enabled)); + fprintf(output, _(" \\ansclean clean query result history\n")); ClosePager(output); } *** a/src/bin/psql/psqlscan.l --- b/src/bin/psql/psqlscan.l *************** *** 737,747 **** other . } else { ! /* ! * if the variable doesn't exist we'll copy the ! * string as is */ ! ECHO; } free(varname); --- 737,760 ---- } else { ! /* ! * This might be the ANS variable */ ! const char* tableName; ! ! tableName = GetOrCreateTable(pset.ans, pset.db, varname); ! if (tableName) ! { ! push_new_buffer(tableName, varname); ! } ! else ! { ! /* ! * if the variable does not exist, copy the ! * string without modification ! */ ! ECHO; ! } } free(varname); *** a/src/bin/psql/settings.h --- b/src/bin/psql/settings.h *************** *** 11,16 **** --- 11,17 ---- #include "variables.h" #include "print.h" + #include "ans.h" #define DEFAULT_FIELD_SEP "|" #define DEFAULT_RECORD_SEP "\n" *************** *** 93,98 **** typedef struct _psqlSettings --- 94,102 ---- FILE *logfile; /* session log file handle */ VariableSpace vars; /* "shell variable" repository */ + + AnsHistory ans; /* query result (answer) history.*/ + bool ans_enabled; /* local query result storage enabled */ /* * The remaining fields are set by assign hooks associated with entries in *** a/src/bin/psql/startup.c --- b/src/bin/psql/startup.c *************** *** 147,152 **** main(int argc, char *argv[]) --- 147,155 ---- SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1); SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2); SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3); + + pset.ans = CreateAnsHistory(); + pset.ans_enabled = 0; parse_psql_options(argc, argv, &options); *** a/src/bin/psql/tab-complete.c --- b/src/bin/psql/tab-complete.c *************** *** 891,897 **** psql_completion(char *text, int start, int end) }; static const char *const backslash_commands[] = { ! "\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright", "\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\df", "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du", --- 891,897 ---- }; static const char *const backslash_commands[] = { ! "\\a", "\\ans", "\\ansclean", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright", "\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\df", "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du",