Hi 2016-11-13 19:46 GMT+01:00 Pavel Stehule <pavel.steh...@gmail.com>:
> Hi > > I am sending a initial implementation of psql content commands. This > design is reaction to Tom's objections against psql file ref variables. > This design is more cleaner, more explicit and more practical - import can > be in one step. > > Now supported commands are: > > r - read file without any modification > rq - read file, escape as literal and use outer quotes > rb - read binary file - transform to hex code string > rbq - read binary file, transform to hex code string and use outer quotes > > Usage: > > create table testt(a xml); > insert into test values( {rq /home/pavel/.local/share/rhythmbox/rhythmdb.xml} > ); > \set xxx {r /home/pavel/.local/share/rhythmbox/rhythmdb.xml} > > This patch is demo of this design - one part is redundant - I'll clean it > in next iteration. > > Regards > > here is cleaned patch * the behave is much more psqlish - show error only when the command is exactly detected - errors are related to processed files only - the behave is similar to psql variables - we doesn't raise a error, when the variable doesn't exists. * no more duplicate code * some basic doc * some basic regress tests Regards Pavel > Pavel > >
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 2410bee..141df6f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2991,6 +2991,57 @@ testdb=> <userinput>\setenv LESS -imx4F</userinput> </refsect3> </refsect2> + <refsect2 id="APP-PSQL-content-commands"> + <title>Content Commands</title> + + <para> + These commands inject some content to processed query. + +<programlisting> +testdb=> <userinput>CREATE TABLE my_table(id int, image bytea);</userinput> +testdb=> <userinput>INSERT INTO my_table VALUES(1, {rbq ~/avatar.gif } ); </userinput> +</programlisting> + + <variablelist> + <varlistentry> + <term><literal>{r <replaceable class="parameter">filename</>}</literal></term> + <listitem> + <para> + Returns content of file. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>{rb <replaceable class="parameter">filename</>}</literal></term> + <listitem> + <para> + Returns content of binary file encoded to hex code. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>{rq <replaceable class="parameter">filename</>}</literal></term> + <listitem> + <para> + Returns content of file, escaped and quoted as string literal + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>{rb <replaceable class="parameter">filename</>}</literal></term> + <listitem> + <para> + Returns content of binary file encoded to hex code and quoted. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect2> + <refsect2> <title>Advanced Features</title> diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index a7fdd8a..43f699f 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -462,6 +462,7 @@ static void setalarm(int seconds); /* callback functions for our flex lexer */ static const PsqlScanCallbacks pgbench_callbacks = { NULL, /* don't need get_variable functionality */ + NULL, /* don't need eval_content_command functionality */ pgbench_error }; diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index a7789df..de7fecd 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -12,6 +12,7 @@ #include <limits.h> #include <math.h> #include <signal.h> +#include <sys/stat.h> #ifndef WIN32 #include <unistd.h> /* for write() */ #else @@ -168,6 +169,211 @@ psql_get_variable(const char *varname, bool escape, bool as_ident) return result; } +#define READFILE_NONE (0) +#define READFILE_NOESCAPE (1 << 0) +#define READFILE_ESCAPE_TEXT (1 << 1) +#define READFILE_ESCAPE_BINARY (1 << 2) +#define READFILE_QUOTE (1 << 3) + +/* + * file-content-fetching callback for read file content commands. + */ +static char * +get_file_content(char *fname, int mode) +{ + FILE *fd; + char *result = NULL; + + expand_tilde(&fname); + canonicalize_path(fname); + + fd = fopen(fname, PG_BINARY_R); + if (fd) + { + struct stat fst; + + if (fstat(fileno(fd), &fst) != -1) + { + if (S_ISREG(fst.st_mode)) + { + if (fst.st_size <= ((int64) 1024) * 1024 * 1024) + { + size_t size; + PQExpBufferData raw_data; + char buf[512]; + + initPQExpBuffer(&raw_data); + + while ((size = fread(buf, 1, sizeof(buf), fd)) > 0) + appendBinaryPQExpBuffer(&raw_data, buf, size); + + if (!ferror(fd) && !(PQExpBufferDataBroken(raw_data))) + { + if (!(mode & READFILE_NOESCAPE)) + { + if (mode & READFILE_ESCAPE_BINARY) + { + unsigned char *escaped_value; + size_t escaped_size; + + escaped_value = PQescapeByteaConn(pset.db, + (const unsigned char *) raw_data.data, raw_data.len, + &escaped_size); + + if (escaped_value) + { + if (mode & READFILE_QUOTE) + { + PQExpBufferData resultbuf; + + initPQExpBuffer(&resultbuf); + + appendPQExpBufferChar(&resultbuf, '\''); + appendBinaryPQExpBuffer(&resultbuf, + (const char *) escaped_value, escaped_size - 1); + appendPQExpBufferChar(&resultbuf, '\''); + PQfreemem(escaped_value); + + if (PQExpBufferDataBroken(resultbuf)) + psql_error("out of memory\n"); + else + result = resultbuf.data; + } + else + result = (char *) escaped_value; + } + else + psql_error("%s\n", PQerrorMessage(pset.db)); + } + else + { + /* + * should be READFILE_ESCAPE_TEXT & READFILE_QUOTE + * escaping of text file has not sense without quoting + */ + result = PQescapeLiteral(pset.db, + raw_data.data, raw_data.len); + if (result == NULL) + psql_error("%s\n", PQerrorMessage(pset.db)); + } + } + else + result = raw_data.data; + } + else + { + if (PQExpBufferDataBroken(raw_data)) + psql_error("out of memory\n"); + else + psql_error("%s: %s\n", fname, strerror(errno)); + } + + if (result != raw_data.data) + termPQExpBuffer(&raw_data); + } + else + psql_error("%s is too big (greather than 1GB)\n", fname); + } + else + psql_error("%s is not regular file\n", fname); + } + else + psql_error("%s: %s\n", fname, strerror(errno)); + + fclose(fd); + } + else + psql_error("%s: %s\n", fname, strerror(errno)); + + return result; +} + +/* + * a implementation of psql content commands - curly bracket + * commands. + * + * It does nothing, when command is not detected - like evaluation of + * psql variables. Usually the SQL statement fails on syntax error - the + * behave is same like evaluation of psql variable. + */ +void +psql_eval_content_command(PQExpBuffer output_buf, int start_offset) +{ + char *content_command; + char *cptr; + int readfile_mode = READFILE_NONE; + + /* do updatable copy of input buffer, input buffer should not be changed */ + content_command = pg_strdup(output_buf->data + start_offset); + + /* skip initial left bracket */ + cptr = content_command + 1; + + /* skip initial spaces */ + while (*cptr == ' ') + cptr++; + + if (*cptr != '\0') + { + char *cname = cptr; + + /* find a end of statement */ + while (*cptr != ' ' && *cptr != '\0') + cptr++; + + /* the space is required in supported commands */ + if (*cptr != '\0') + { + *cptr++ = '\0'; + + if (strcmp(cname, "r") == 0) + readfile_mode = READFILE_NOESCAPE; + else if (strcmp(cname, "rb") == 0) + readfile_mode = READFILE_ESCAPE_BINARY; + else if (strcmp(cname, "rq") == 0) + readfile_mode = READFILE_ESCAPE_TEXT | READFILE_QUOTE; + else if (strcmp(cname, "rbq") == 0) + readfile_mode = READFILE_ESCAPE_BINARY | READFILE_QUOTE; + + if (readfile_mode != READFILE_NONE) + { + /* parse file name from the rest of content command */ + + /* skip initial spaces */ + while (*cptr == ' ') + cptr++; + + if (cptr != '\0') + { + char *eptr = cptr + strlen(cptr) - 1 - 1; /* drop right bracket */ + + while (*eptr == ' ' && eptr > cptr) + eptr--; + + eptr[1] = '\0'; + + if (cptr != '\0') + { + char *result = get_file_content(cptr, readfile_mode); + + if (result != NULL) + { + /* remove content command text from output buffer */ + output_buf->len = start_offset; + output_buf->data[output_buf->len] = '\0'; + + /* append the result of content command */ + appendPQExpBufferStr(output_buf, result); + PQfreemem(result); + } + } + } + } + } + } + + PQfreemem(content_command); +} /* * Error reporting for scripts. Errors should look like diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h index bdcb58f..18a9c86 100644 --- a/src/bin/psql/common.h +++ b/src/bin/psql/common.h @@ -11,6 +11,7 @@ #include <setjmp.h> #include "libpq-fe.h" +#include "pqexpbuffer.h" #include "fe_utils/print.h" #define atooid(x) ((Oid) strtoul((x), NULL, 10)) @@ -19,6 +20,7 @@ extern bool openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe); extern bool setQFout(const char *fname); extern char *psql_get_variable(const char *varname, bool escape, bool as_ident); +extern void psql_eval_content_command(PQExpBuffer output_buf, int start_offset); extern void psql_error(const char *fmt,...) pg_attribute_printf(1, 2); diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c index 37dfa4d..69c697b 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -20,6 +20,7 @@ /* callback functions for our flex lexer */ const PsqlScanCallbacks psqlscan_callbacks = { psql_get_variable, + psql_eval_content_command, psql_error }; diff --git a/src/bin/psql/psqlscanslash.l b/src/bin/psql/psqlscanslash.l index 86832a8..06b34a4 100644 --- a/src/bin/psql/psqlscanslash.l +++ b/src/bin/psql/psqlscanslash.l @@ -19,6 +19,7 @@ #include "postgres_fe.h" #include "psqlscanslash.h" +#include "common.h" #include "libpq-fe.h" } @@ -46,6 +47,7 @@ static enum slash_option_type option_type; static char *option_quote; static int unquoted_option_chars; static int backtick_start_offset; +static int curlybracket_start_offset; /* Return values from yylex() */ @@ -98,6 +100,7 @@ extern void slash_yyset_column(int column_no, yyscan_t yyscanner); %x xslashdquote %x xslashwholeline %x xslashend +%x xslashcurlybrackets /* * Assorted character class definitions that should match psqlscan.l. @@ -228,6 +231,14 @@ other . BEGIN(xslashdquote); } +"{" { + curlybracket_start_offset = output_buf->len; + ECHO; + *option_quote = '{'; + unquoted_option_chars = 0; + BEGIN(xslashcurlybrackets); + } + :{variable_char}+ { /* Possible psql variable substitution */ if (option_type == OT_NO_EVAL || @@ -362,6 +373,25 @@ other . } +<xslashcurlybrackets>{ + /* curly brackets: copy everything until next right curly bracket */ + +"}" { + /* In NO_EVAL mode, don't evaluate the command */ + ECHO; + if (option_type != OT_NO_EVAL) + { + if (cur_state->callbacks->eval_content_command) + cur_state->callbacks->eval_content_command(cur_state->output_buf, + curlybracket_start_offset); + } + BEGIN(xslasharg); + } + +{other}|\n { ECHO; } + +} + <xslashdquote>{ /* double-quoted text: copy verbatim, including the double quotes */ @@ -580,6 +610,7 @@ psql_scan_slash_option(PsqlScanState state, case xslashquote: case xslashbackquote: case xslashdquote: + case xslashcurlybrackets: /* must have hit EOL inside quotes */ state->callbacks->write_error("unterminated quoted string\n"); termPQExpBuffer(&mybuf); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index b556c00..b8d6c83 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -58,7 +58,7 @@ extern char *filename_completion_function(); #endif /* word break characters */ -#define WORD_BREAKS "\t\n@$><=;|&{() " +#define WORD_BREAKS "\t\n@$><=;|&() " /* * Since readline doesn't let us pass any state through to the tab completion @@ -1343,6 +1343,11 @@ psql_completion(const char *text, int start, int end) "\\timing", "\\unset", "\\x", "\\w", "\\watch", "\\z", "\\!", NULL }; + /* psql's content commands. */ + static const char *const content_commands[] = { + "{r", "{rq", "{rb", "{rbq", NULL + }; + (void) end; /* "end" is not used */ #ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER @@ -1368,6 +1373,10 @@ psql_completion(const char *text, int start, int end) if (text[0] == '\\') COMPLETE_WITH_LIST_CS(backslash_commands); + else if (text[0] == '{') + { + COMPLETE_WITH_LIST_CS(content_commands); + } /* If current word is a variable interpolation, handle that case */ else if (text[0] == ':' && text[1] != ':') { @@ -3222,6 +3231,11 @@ psql_completion(const char *text, int start, int end) completion_charp = "\\"; matches = completion_matches(text, complete_from_files); } + else if (TailMatchesCS1("\{r|\{rb|\{rq|\{rbq")) + { + completion_charp = "{"; + matches = completion_matches(text, complete_from_files); + } /* * Finally, we look through the list of "things", such as TABLE, INDEX and diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l index 55067b4..49d6dae 100644 --- a/src/fe_utils/psqlscan.l +++ b/src/fe_utils/psqlscan.l @@ -47,6 +47,8 @@ */ typedef int YYSTYPE; +static int curlybracket_start_offset; + /* * Set the type of yyextra; we use it as a pointer back to the containing * PsqlScanState. @@ -115,6 +117,7 @@ extern void psql_yyset_column(int column_no, yyscan_t yyscanner); * <xuiend> end of a quoted identifier with Unicode escapes, UESCAPE can follow * <xus> quoted string with Unicode escapes * <xusend> end of a quoted string with Unicode escapes, UESCAPE can follow + * <xcb> curly bracket string * * Note: we intentionally don't mimic the backend's <xeu> state; we have * no need to distinguish it from <xe> state, and no good way to get out @@ -133,6 +136,7 @@ extern void psql_yyset_column(int column_no, yyscan_t yyscanner); %x xuiend %x xus %x xusend +%x xcb /* * In order to make the world safe for Windows and Mac clients as well as @@ -673,6 +677,24 @@ other . } } +"{" { + curlybracket_start_offset = cur_state->output_buf->len; + BEGIN(xcb); + ECHO; + } + +<xcb>"}" { + BEGIN(INITIAL); + ECHO; + if (cur_state->callbacks->eval_content_command) + cur_state->callbacks->eval_content_command(cur_state->output_buf, + curlybracket_start_offset); + } + +<xcb>.|\n { + ECHO; + } + /* * psql-specific rules to handle backslash commands and variable * substitution. We want these before {self}, also. diff --git a/src/include/fe_utils/psqlscan.h b/src/include/fe_utils/psqlscan.h index 1f10ecc..a70a40e 100644 --- a/src/include/fe_utils/psqlscan.h +++ b/src/include/fe_utils/psqlscan.h @@ -54,6 +54,8 @@ typedef struct PsqlScanCallbacks /* Fetch value of a variable, as a pfree'able string; NULL if unknown */ /* This pointer can be NULL if no variable substitution is wanted */ char *(*get_variable) (const char *varname, bool escape, bool as_ident); + /* eval psql content command */ + void (*eval_content_command) (PQExpBuffer output_buf, int start_offset); /* Print an error message someplace appropriate */ /* (very old gcc versions don't support attributes on function pointers) */ #if defined(__GNUC__) && __GNUC__ < 4 diff --git a/src/test/regress/input/misc.source b/src/test/regress/input/misc.source index dd2d1b2..bca752f 100644 --- a/src/test/regress/input/misc.source +++ b/src/test/regress/input/misc.source @@ -273,3 +273,21 @@ drop table oldstyle_test; -- -- rewrite rules -- + +-- +-- psql content commands +-- +CREATE TABLE test_setref(a text, b bytea); + +-- use two different ways for import data - result should be same +\lo_import @abs_builddir@/data/hash.data +\set lo_oid :LASTOID +INSERT INTO test_setref (b) VALUES(lo_get(:lo_oid)); +\lo_unlink :lo_oid +SELECT md5(b) FROM test_setref; +TRUNCATE test_setref; + +INSERT INTO test_setref(a,b) VALUES({rq @abs_builddir@/data/hash.data}, {rbq @abs_builddir@/data/hash.data}); +SELECT md5(a), md5(b) FROM test_setref; + +DROP TABLE test_setref; diff --git a/src/test/regress/output/misc.source b/src/test/regress/output/misc.source index 574ef0d..0c8eb97 100644 --- a/src/test/regress/output/misc.source +++ b/src/test/regress/output/misc.source @@ -708,3 +708,27 @@ drop table oldstyle_test; -- -- rewrite rules -- +-- +-- psql content commands +-- +CREATE TABLE test_setref(a text, b bytea); +-- use two different ways for import data - result should be same +\lo_import @abs_builddir@/data/hash.data +\set lo_oid :LASTOID +INSERT INTO test_setref (b) VALUES(lo_get(:lo_oid)); +\lo_unlink :lo_oid +SELECT md5(b) FROM test_setref; + md5 +---------------------------------- + e446fe6ea5a347e69670633412c7f8cb +(1 row) + +TRUNCATE test_setref; +INSERT INTO test_setref(a,b) VALUES({rq @abs_builddir@/data/hash.data}, {rbq @abs_builddir@/data/hash.data}); +SELECT md5(a), md5(b) FROM test_setref; + md5 | md5 +----------------------------------+---------------------------------- + e446fe6ea5a347e69670633412c7f8cb | e446fe6ea5a347e69670633412c7f8cb +(1 row) + +DROP TABLE test_setref;
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers