diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile index 5b77173..1654607 100644 --- a/src/bin/psql/Makefile +++ b/src/bin/psql/Makefile @@ -20,7 +20,7 @@ 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 \ +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 \ diff --git a/src/bin/psql/ans.c b/src/bin/psql/ans.c new file mode 100644 index 0000000..88b8780 --- /dev/null +++ b/src/bin/psql/ans.c @@ -0,0 +1,348 @@ +/* + * 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; + + 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; + + /* name */ + 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; +} + +/* 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 + { + /* TODO: escaping! */ + bufferSize += strlen(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); + /* TODO: escaping! */ + strcpy(dataPtr, value); + dataPtr += strlen(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; + } + } + +} diff --git a/src/bin/psql/ans.h b/src/bin/psql/ans.h new file mode 100644 index 0000000..88faf5c --- /dev/null +++ b/src/bin/psql/ans.h @@ -0,0 +1,47 @@ +/* + * 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" + + +/* Cache of last N query results, stored in a format ready to insert itno tempary table when needed */ +struct _ans +{ + int numColumns; + Oid *columnTypes; + char **columnNames; + char *data; + + char *name; // hiostory item name + char *tableName; // corresponding table in DB. NULL if not created yet + + struct _ans *next; +}; + +typedef struct _ans* AnsHistory; + +AnsHistory CreateAnsHistory(void); +void AddToHistory(AnsHistory history, PGresult* result); + + +/* + * Takes variable name and checks wether it matches the name of existing history item. + * If none found, returns NULL. + * If found, but table not created yet, creates the table first. + */ +const char* GetOrCreateTable(AnsHistory history, PGconn *db, const char* name); + +/* Should be called when new connection is open. + * Because all the tables used by ANS are temporary and kept in current DB, thety are no longer valid + * after the client connect to new database + */ +void AnsClearTableNames(AnsHistory history); + +#endif diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0e99794..5488c22 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -40,6 +40,7 @@ #include "pqexpbuffer.h" #include "dumputils.h" +#include "ans.h" #include "common.h" #include "copy.h" #include "describe.h" @@ -51,6 +52,7 @@ #include "psqlscan.h" #include "settings.h" #include "variables.h" +#include "ans.h" /* functions for use in this file */ @@ -1697,6 +1699,7 @@ do_connect(char *dbname, char *user, char *host, char *port) 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 */ diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 3dea92c..8b1c1ec 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -23,6 +23,7 @@ #include "command.h" #include "copy.h" #include "mbprint.h" +#include "ans.h" @@ -950,7 +951,11 @@ SendQuery(const char *query) /* but printing results isn't: */ if (OK && results) + { OK = PrintQueryResults(results); + + AddToHistory(pset.ans, results); + } } else { @@ -1227,7 +1232,7 @@ 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 */ diff --git a/src/bin/psql/psqlscan.l b/src/bin/psql/psqlscan.l index d61387d..686fbcc 100644 --- a/src/bin/psql/psqlscan.l +++ b/src/bin/psql/psqlscan.l @@ -737,11 +737,24 @@ other . } else { - /* - * if the variable doesn't exist we'll copy the - * string as is + /* + * This could be ANS variable, check it. */ - ECHO; + const char* tableName; + + tableName = GetOrCreateTable(pset.ans, pset.db, varname); + if (tableName) + { + push_new_buffer(tableName, varname); + } + else + { + /* + * if the variable doesn't exist we'll copy the + * string as is + */ + ECHO; + } } free(varname); diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index e78aa9a..ef69184 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -11,6 +11,7 @@ #include "variables.h" #include "print.h" +#include "ans.h" #define DEFAULT_FIELD_SEP "|" #define DEFAULT_RECORD_SEP "\n" @@ -93,6 +94,8 @@ typedef struct _psqlSettings FILE *logfile; /* session log file handle */ VariableSpace vars; /* "shell variable" repository */ + + AnsHistory ans; /* query result (answer) history */ /* * The remaining fields are set by assign hooks associated with entries in diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 5d7fe6e..b0e213c 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -147,6 +147,8 @@ main(int argc, char *argv[]) SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1); SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2); SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3); + + pset.ans = CreateAnsHistory(); parse_psql_options(argc, argv, &options);