diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index d644a46..d1cca1e 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -588,6 +588,7 @@ fileBeginForeignScan(ForeignScanState *node, int eflags) */ cstate = BeginCopyFrom(node->ss.ss_currentRelation, filename, + false, NIL, options); @@ -660,6 +661,7 @@ fileReScanForeignScan(ForeignScanState *node) festate->cstate = BeginCopyFrom(node->ss.ss_currentRelation, festate->filename, + false, NIL, festate->options); } @@ -993,7 +995,7 @@ file_acquire_sample_rows(Relation onerel, int elevel, /* * Create CopyState from FDW options. */ - cstate = BeginCopyFrom(onerel, filename, NIL, options); + cstate = BeginCopyFrom(onerel, filename, false, NIL, options); /* * Use per-tuple memory context to prevent leak of memory used to read diff --git a/doc/src/sgml/keywords.sgml b/doc/src/sgml/keywords.sgml index 0e7b322..576fd65 100644 --- a/doc/src/sgml/keywords.sgml +++ b/doc/src/sgml/keywords.sgml @@ -3514,6 +3514,13 @@ reserved + PROGRAM + non-reserved + + + + + PUBLIC non-reserved diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 2137c67..a00e636 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -23,11 +23,11 @@ PostgreSQL documentation COPY table_name [ ( column_name [, ...] ) ] - FROM { 'filename' | STDIN } + FROM { 'filename' | PROGRAM 'command' | STDIN } [ [ WITH ] ( option [, ...] ) ] COPY { table_name [ ( column_name [, ...] ) ] | ( query ) } - TO { 'filename' | STDOUT } + TO { 'filename' | PROGRAM 'command' | STDOUT } [ [ WITH ] ( option [, ...] ) ] where option can be one of: @@ -71,10 +71,14 @@ COPY { table_name [ ( COPY with a file name instructs the PostgreSQL server to directly read from or write to a file. The file must be accessible to the server and - the name must be specified from the viewpoint of the server. When - STDIN or STDOUT is - specified, data is transmitted via the connection between the - client and the server. + the name must be specified from the viewpoint of the server. + COPY with a command instructs the + PostgreSQL server to directly execute + the command that input comes from or output goes to. The command must + be executable by the server and specified from the viewpoint of the server. + When STDIN or STDOUT is + specified, data is transmitted via the connection between the client + and the server. @@ -126,6 +130,18 @@ COPY { table_name [ ( + command + + + The pre- or post-processor command. A pre-processor command in + COPY FROM must write its output to standard output. + A post-processor command in COPY TO must read its + input from standard input. + + + + + STDIN @@ -394,6 +410,25 @@ COPY count + A pre- or post-processor command specified in a COPY + command is executed directly by the server, not by the client application. + Therefore, it must be executable at the database server machine, + not the client. It must be executable by the + PostgreSQL user, not the client. + COPY specifying a pre- or post-processor command is + only allowed to database superusers. It is recommended that the command + specified in COPY be set using an absolute paths. + However, a relative path can be specified, which will be interpreted + relative to the working directory of the server process. + + + + A pre- or post-processor command specified in a COPY + command might not be executed in operating systems that implement + access control for their resources such as the SELinux operating system. + + + COPY FROM will invoke any triggers and check constraints on the destination table. However, it will not invoke rules. @@ -842,6 +877,14 @@ COPY (SELECT * FROM country WHERE country_name LIKE 'A%') TO '/usr1/proj/bray/sq + The PROGRAM option with the following post-processor command allows you + to compress the file: + +COPY (SELECT * FROM country WHERE country_name LIKE 'A%') TO PROGRAM '/bin/gzip > /usr1/proj/bray/sql/a_list_countries.copy.gz'; + + + + Here is a sample of data suitable for copying into a table from STDIN: diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 233f747..e74c82f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -829,7 +829,7 @@ testdb=> \copy { table [ ( column_list ) ] | ( query ) } { from | to } - { filename | stdin | stdout | pstdin | pstdout } + { filename | program command | stdin | stdout | pstdin | pstdout } [ [ with ] ( option [, ...] ) ] @@ -846,6 +846,24 @@ testdb=> + When program is specified, + command is + directly executed by psql, + not the server, and the data from or to + command is + routed between the server and the client. + This means that the exectution privileges are those of + the local user, not the server, and no SQL superuser + privileges are required. Note that + psql might terminate without + any error messages when + command + does not work properly. In such a case, make sure + command + works in itself. + + + The syntax of the command is similar to that of the SQL command, and diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 523c1e0..9a79824 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -108,6 +108,7 @@ typedef struct CopyStateData QueryDesc *queryDesc; /* executable query to copy from */ List *attnumlist; /* integer list of attnums to copy */ char *filename; /* filename, or NULL for STDIN/STDOUT */ + bool is_program; /* do I execute popen/pclose? */ bool binary; /* binary format? */ bool oids; /* include OIDs? */ bool freeze; /* freeze rows on loading? */ @@ -278,7 +279,8 @@ static CopyState BeginCopy(bool is_from, Relation rel, Node *raw_query, const char *queryString, List *attnamelist, List *options); static void EndCopy(CopyState cstate); static CopyState BeginCopyTo(Relation rel, Node *query, const char *queryString, - const char *filename, List *attnamelist, List *options); + const char *filename, bool is_program, + List *attnamelist, List *options); static void EndCopyTo(CopyState cstate); static uint64 DoCopyTo(CopyState cstate); static uint64 CopyTo(CopyState cstate); @@ -812,15 +814,16 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed) if (XactReadOnly && !rel->rd_islocaltemp) PreventCommandIfReadOnly("COPY FROM"); - cstate = BeginCopyFrom(rel, stmt->filename, + cstate = BeginCopyFrom(rel, stmt->filename, stmt->is_program, stmt->attlist, stmt->options); *processed = CopyFrom(cstate); /* copy from file to database */ EndCopyFrom(cstate); } else { - cstate = BeginCopyTo(rel, stmt->query, queryString, stmt->filename, - stmt->attlist, stmt->options); + cstate = BeginCopyTo(rel, stmt->query, queryString, + stmt->filename, stmt->is_program, + stmt->attlist, stmt->options); *processed = DoCopyTo(cstate); /* copy from database to file */ EndCopyTo(cstate); } @@ -1395,11 +1398,21 @@ BeginCopy(bool is_from, static void EndCopy(CopyState cstate) { - if (cstate->filename != NULL && FreeFile(cstate->copy_file)) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not close file \"%s\": %m", - cstate->filename))); + if (cstate->is_program) + { + if (ClosePipeStream(cstate->copy_file) == -1) + ereport(ERROR, + (errmsg("could not execute command \"%s\"", + cstate->filename))); + } + else + { + if (cstate->filename != NULL && FreeFile(cstate->copy_file)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + cstate->filename))); + } MemoryContextDelete(cstate->copycontext); pfree(cstate); @@ -1413,6 +1426,7 @@ BeginCopyTo(Relation rel, Node *query, const char *queryString, const char *filename, + bool is_program, List *attnamelist, List *options) { @@ -1449,6 +1463,12 @@ BeginCopyTo(Relation rel, cstate = BeginCopy(false, rel, query, queryString, attnamelist, options); oldcontext = MemoryContextSwitchTo(cstate->copycontext); + cstate->is_program = is_program; + if (cstate->is_program && pipe) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PROGRAM is not supported to stdout or from stdin"))); + if (pipe) { if (whereToSendOutput != DestRemote) @@ -1456,34 +1476,45 @@ BeginCopyTo(Relation rel, } else { - mode_t oumask; /* Pre-existing umask value */ - struct stat st; + cstate->filename = pstrdup(filename); - /* - * Prevent write to relative path ... too easy to shoot oneself in the - * foot by overwriting a database file ... - */ - if (!is_absolute_path(filename)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_NAME), - errmsg("relative path not allowed for COPY to file"))); + if (cstate->is_program) + { + cstate->copy_file = OpenPipeStream(cstate->filename, PG_BINARY_W); + if (cstate->copy_file == NULL) + ereport(ERROR, + (errmsg("could not execute command \"%s\": %m", + cstate->filename))); + } + else + { + mode_t oumask; /* Pre-existing umask value */ + struct stat st; - cstate->filename = pstrdup(filename); - oumask = umask(S_IWGRP | S_IWOTH); - cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_W); - umask(oumask); + /* + * Prevent write to relative path ... too easy to shoot oneself in + * the foot by overwriting a database file ... + */ + if (!is_absolute_path(filename)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("relative path not allowed for COPY to file"))); - if (cstate->copy_file == NULL) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not open file \"%s\" for writing: %m", - cstate->filename))); + oumask = umask(S_IWGRP | S_IWOTH); + cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_W); + umask(oumask); + if (cstate->copy_file == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" for writing: %m", + cstate->filename))); - fstat(fileno(cstate->copy_file), &st); - if (S_ISDIR(st.st_mode)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a directory", cstate->filename))); + fstat(fileno(cstate->copy_file), &st); + if (S_ISDIR(st.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a directory", cstate->filename))); + } } MemoryContextSwitchTo(oldcontext); @@ -2317,6 +2348,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, CopyState BeginCopyFrom(Relation rel, const char *filename, + bool is_program, List *attnamelist, List *options) { @@ -2414,6 +2446,12 @@ BeginCopyFrom(Relation rel, cstate->volatile_defexprs = volatile_defexprs; cstate->num_defaults = num_defaults; + cstate->is_program = is_program; + if (cstate->is_program && pipe) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PROGRAM is not supported to stdout or from stdin"))); + if (pipe) { if (whereToSendOutput == DestRemote) @@ -2423,22 +2461,33 @@ BeginCopyFrom(Relation rel, } else { - struct stat st; - cstate->filename = pstrdup(filename); - cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_R); - if (cstate->copy_file == NULL) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not open file \"%s\" for reading: %m", - cstate->filename))); + if (cstate->is_program) + { + cstate->copy_file = OpenPipeStream(cstate->filename, PG_BINARY_R); + if (cstate->copy_file == NULL) + ereport(ERROR, + (errmsg("could not execute command \"%s\": %m", + cstate->filename))); + } + else + { + struct stat st; - fstat(fileno(cstate->copy_file), &st); - if (S_ISDIR(st.st_mode)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a directory", cstate->filename))); + cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_R); + if (cstate->copy_file == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" for reading: %m", + cstate->filename))); + + fstat(fileno(cstate->copy_file), &st); + if (S_ISDIR(st.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a directory", cstate->filename))); + } } if (!cstate->binary) diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 2da08d1..23ec88d 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2703,6 +2703,7 @@ _copyCopyStmt(const CopyStmt *from) COPY_NODE_FIELD(query); COPY_NODE_FIELD(attlist); COPY_SCALAR_FIELD(is_from); + COPY_SCALAR_FIELD(is_program); COPY_STRING_FIELD(filename); COPY_NODE_FIELD(options); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 9e313c8..99c034a 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1090,6 +1090,7 @@ _equalCopyStmt(const CopyStmt *a, const CopyStmt *b) COMPARE_NODE_FIELD(query); COMPARE_NODE_FIELD(attlist); COMPARE_SCALAR_FIELD(is_from); + COMPARE_SCALAR_FIELD(is_program); COMPARE_STRING_FIELD(filename); COMPARE_NODE_FIELD(options); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b998431..edf2457 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -381,7 +381,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_freeze opt_default opt_recheck %type opt_binary opt_oids copy_delimiter -%type copy_from +%type copy_from copy_program %type opt_column event cursor_options opt_hold opt_set_data %type reindex_type drop_type comment_type security_label_type @@ -568,7 +568,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY - PRIOR PRIVILEGES PROCEDURAL PROCEDURE + PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM QUOTE @@ -2309,7 +2309,9 @@ ClosePortalStmt: * * QUERY : * COPY relname [(columnList)] FROM/TO file [WITH] [(options)] + * COPY relname [(columnList)] FROM/TO PROGRAM command [WITH] [(options)] * COPY ( SELECT ... ) TO file [WITH] [(options)] + * COPY ( SELECT ... ) TO PROGRAM command [WITH] [(options)] * * In the preferred syntax the options are comma-separated * and use generic identifiers instead of keywords. The pre-9.0 @@ -2324,14 +2326,15 @@ ClosePortalStmt: *****************************************************************************/ CopyStmt: COPY opt_binary qualified_name opt_column_list opt_oids - copy_from copy_file_name copy_delimiter opt_with copy_options + copy_from copy_program copy_file_name copy_delimiter opt_with copy_options { CopyStmt *n = makeNode(CopyStmt); n->relation = $3; n->query = NULL; n->attlist = $4; n->is_from = $6; - n->filename = $7; + n->is_program = $7; + n->filename = $8; n->options = NIL; /* Concatenate user-supplied flags */ @@ -2339,21 +2342,22 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list opt_oids n->options = lappend(n->options, $2); if ($5) n->options = lappend(n->options, $5); - if ($8) - n->options = lappend(n->options, $8); - if ($10) - n->options = list_concat(n->options, $10); + if ($9) + n->options = lappend(n->options, $9); + if ($11) + n->options = list_concat(n->options, $11); $$ = (Node *)n; } - | COPY select_with_parens TO copy_file_name opt_with copy_options + | COPY select_with_parens TO copy_program copy_file_name opt_with copy_options { CopyStmt *n = makeNode(CopyStmt); n->relation = NULL; n->query = $2; n->attlist = NIL; n->is_from = false; - n->filename = $4; - n->options = $6; + n->is_program = $4; + n->filename = $5; + n->options = $7; $$ = (Node *)n; } ; @@ -2363,6 +2367,11 @@ copy_from: | TO { $$ = FALSE; } ; +copy_program: + PROGRAM { $$ = TRUE; } + | /* EMPTY */ { $$ = FALSE; } + ; + /* * copy_file_name NULL indicates stdio is used. Whether stdin or stdout is * used depends on the direction. (It really doesn't make sense to copy from @@ -12666,6 +12675,7 @@ unreserved_keyword: | PRIVILEGES | PROCEDURAL | PROCEDURE + | PROGRAM | QUOTE | RANGE | READ diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index ff7e221..71fd656 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -39,10 +39,11 @@ * for a long time, like relation files. It is the caller's responsibility * to close them, there is no automatic mechanism in fd.c for that. * - * AllocateFile, AllocateDir and OpenTransientFile are wrappers around - * fopen(3), opendir(3), and open(2), respectively. They behave like the - * corresponding native functions, except that the handle is registered with - * the current subtransaction, and will be automatically closed at abort. + * AllocateFile, AllocateDir, OpenPipeStream and OpenTransientFile are + * wrappers around fopen(3), opendir(3), popen(3) and open(2), respectively. + * They behave like the corresponding native functions, except that the handle + * is registered with the current subtransaction, and will be automatically + * closed at abort. * These are intended for short operations like reading a configuration file, * and there is a fixed limit on the number of files that can be opened using * these functions at any one time. @@ -201,6 +202,7 @@ static uint64 temporary_files_size = 0; typedef enum { AllocateDescFile, + AllocateDescPipe, AllocateDescDir, AllocateDescRawFD } AllocateDescKind; @@ -1600,6 +1602,9 @@ FreeDesc(AllocateDesc *desc) case AllocateDescFile: result = fclose(desc->desc.file); break; + case AllocateDescPipe: + result = pclose_check(desc->desc.file); + break; case AllocateDescDir: result = closedir(desc->desc.dir); break; @@ -1812,6 +1817,85 @@ FreeDir(DIR *dir) return closedir(dir); } +/* + * Routines that want to initiate a pipe stream should use OpenPipeStream + * rather than plain popen(). This lets fd.c deal with freeing FDs if + * necessary. When done, call ClosePipeStream rather than pclose. + */ +FILE * +OpenPipeStream(const char *command, const char *mode) +{ + FILE *file; + + DO_DB(elog(LOG, "OpenPipeStream: Allocated %d (%s)", + numAllocatedDescs, command)); + + /* + * The test against MAX_ALLOCATED_DESCS prevents us from overflowing + * allocatedFiles[]; the test against max_safe_fds prevents AllocateFile + * from hogging every one of the available FDs, which'd lead to infinite + * looping. + */ + if (numAllocatedDescs >= MAX_ALLOCATED_DESCS || + numAllocatedDescs >= max_safe_fds - 1) + elog(ERROR, "exceeded MAX_ALLOCATED_DESCS while trying to execute command \"%s\"", + command); + +TryAgain: + fflush(stdout); + fflush(stderr); + errno = 0; + if ((file = popen(command, mode)) != NULL) + { + AllocateDesc *desc = &allocatedDescs[numAllocatedDescs]; + + desc->kind = AllocateDescPipe; + desc->desc.file = file; + desc->create_subid = GetCurrentSubTransactionId(); + numAllocatedDescs++; + return desc->desc.file; + } + + if (errno == EMFILE || errno == ENFILE) + { + int save_errno = errno; + + ereport(LOG, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("out of file descriptors: %m; release and retry"))); + errno = 0; + if (ReleaseLruFile()) + goto TryAgain; + errno = save_errno; + } + + return NULL; +} + +/* + * Close a pipe stream returned by OpenPipeStream. + */ +int +ClosePipeStream(FILE *file) +{ + int i; + + DO_DB(elog(LOG, "ClosePipeStream: Allocated %d", numAllocatedDescs)); + + /* Remove file from list of allocated files, if it's present */ + for (i = numAllocatedDescs; --i >= 0;) + { + AllocateDesc *desc = &allocatedDescs[i]; + + if (desc->kind == AllocateDescPipe && desc->desc.file == file) + return FreeDesc(desc); + } + + /* Only get here if someone passes us a file not in allocatedDescs */ + elog(WARNING, "file passed to ClosePipeStream was not obtained from OpenPipeStream"); + + return pclose_check(file); +} /* * closeAllVfds diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index a31d789..f7f1803 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -32,8 +32,8 @@ * -- parses \copy command line * * The documented syntax is: - * \copy tablename [(columnlist)] from|to filename [options] - * \copy ( select stmt ) to filename [options] + * \copy tablename [(columnlist)] from|to {filename|program command} [options] + * \copy ( select stmt ) to {filename|program command} [options] * * An undocumented fact is that you can still write BINARY before the * tablename; this is a hangover from the pre-7.3 syntax. The options @@ -43,6 +43,7 @@ * table name can be double-quoted and can have a schema part. * column names can be double-quoted. * filename can be single-quoted like SQL literals. + * command can be single-quoted like SQL literals. * * returns a malloc'ed structure with the options, or NULL on parsing error */ @@ -52,6 +53,7 @@ struct copy_options char *before_tofrom; /* COPY string before TO/FROM */ char *after_tofrom; /* COPY string after TO/FROM filename */ char *file; /* NULL = stdin/stdout */ + bool program; /* do I execute popen/pclose? */ bool psql_inout; /* true = use psql stdin/stdout */ bool from; /* true = FROM, false = TO */ }; @@ -191,11 +193,25 @@ parse_slash_copy(const char *args) else goto error; - token = strtokx(NULL, whitespace, NULL, "'", - 0, false, true, pset.encoding); + token = strtokx(NULL, whitespace, ";", NULL, + 0, false, false, pset.encoding); if (!token) goto error; + if (pg_strcasecmp(token, "program") == 0) + { + result->program = true; + token = strtokx(NULL, whitespace, NULL, "'", + 0, false, true, pset.encoding); + if (!token) + goto error; + } + else + { + result->program = false; + strip_quotes(token, '\'', 0, pset.encoding); + } + if (pg_strcasecmp(token, "stdin") == 0 || pg_strcasecmp(token, "stdout") == 0) { @@ -212,7 +228,8 @@ parse_slash_copy(const char *args) { result->psql_inout = false; result->file = pg_strdup(token); - expand_tilde(&result->file); + if (!result->program) + expand_tilde(&result->file); } /* Collect the rest of the line (COPY options) */ @@ -237,7 +254,10 @@ error: /* * Execute a \copy command (frontend copy). We have to open a file, then * submit a COPY query to the backend and either feed it data from the - * file or route its response into the file. + * file or route its response into the file. When the program option is + * specified, we have to create a pipe instead of opening a file and + * either read data from the pipe into the backend or write its response + * into the pipe. */ bool do_copy(const char *args) @@ -256,8 +276,15 @@ do_copy(const char *args) if (!options) return false; + if (options->file == NULL && options->program) + { + psql_error("program is not supported to stdout/pstdout or from stdin/pstdin\n"); + free_copy_options(options); + return false; + } + /* prepare to read or write the target file */ - if (options->file) + if (options->file && !options->program) canonicalize_path(options->file); if (options->from) @@ -265,7 +292,17 @@ do_copy(const char *args) override_file = &pset.cur_cmd_source; if (options->file) - copystream = fopen(options->file, PG_BINARY_R); + { + if (options->program) + { + fflush(stdout); + fflush(stderr); + errno = 0; + copystream = popen(options->file, PG_BINARY_R); + } + else + copystream = fopen(options->file, PG_BINARY_R); + } else if (!options->psql_inout) copystream = pset.cur_cmd_source; else @@ -276,7 +313,17 @@ do_copy(const char *args) override_file = &pset.queryFout; if (options->file) - copystream = fopen(options->file, PG_BINARY_W); + { + if (options->program) + { + fflush(stdout); + fflush(stderr); + errno = 0; + copystream = popen(options->file, PG_BINARY_W); + } + else + copystream = fopen(options->file, PG_BINARY_W); + } else if (!options->psql_inout) copystream = pset.queryFout; else @@ -285,21 +332,28 @@ do_copy(const char *args) if (!copystream) { - psql_error("%s: %s\n", - options->file, strerror(errno)); + if (options->program) + psql_error("could not execute command \"%s\": %s\n", + options->file, strerror(errno)); + else + psql_error("%s: %s\n", + options->file, strerror(errno)); free_copy_options(options); return false; } - /* make sure the specified file is not a directory */ - fstat(fileno(copystream), &st); - if (S_ISDIR(st.st_mode)) + if (!options->program) { - fclose(copystream); - psql_error("%s: cannot copy from/to a directory\n", - options->file); - free_copy_options(options); - return false; + /* make sure the specified file is not a directory */ + fstat(fileno(copystream), &st); + if (S_ISDIR(st.st_mode)) + { + fclose(copystream); + psql_error("%s: cannot copy from/to a directory\n", + options->file); + free_copy_options(options); + return false; + } } /* build the command we will send to the backend */ @@ -322,10 +376,22 @@ do_copy(const char *args) if (options->file != NULL) { - if (fclose(copystream) != 0) + if (options->program) { - psql_error("%s: %s\n", options->file, strerror(errno)); - success = false; + if (pclose_check(copystream) == -1) + { + psql_error("could not execute command \"%s\"\n", + options->file); + success = false; + } + } + else + { + if (fclose(copystream) != 0) + { + psql_error("%s: %s\n", options->file, strerror(errno)); + success = false; + } } } free_copy_options(options); diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c index 450240d..99968a1 100644 --- a/src/bin/psql/stringutils.c +++ b/src/bin/psql/stringutils.c @@ -13,9 +13,6 @@ #include "stringutils.h" -static void strip_quotes(char *source, char quote, char escape, int encoding); - - /* * Replacement for strtok() (a.k.a. poor man's flex) * @@ -239,7 +236,7 @@ strtokx(const char *s, * * Note that the source string is overwritten in-place. */ -static void +void strip_quotes(char *source, char quote, char escape, int encoding) { char *src; diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h index b991376..bb2a194 100644 --- a/src/bin/psql/stringutils.h +++ b/src/bin/psql/stringutils.h @@ -19,6 +19,8 @@ extern char *strtokx(const char *s, bool del_quotes, int encoding); +extern void strip_quotes(char *source, char quote, char escape, int encoding); + extern char *quote_if_needed(const char *source, const char *entails_quote, char quote, char escape, int encoding); diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index 725c277..5860e4c 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -26,7 +26,7 @@ extern Oid DoCopy(const CopyStmt *stmt, const char *queryString, extern void ProcessCopyOptions(CopyState cstate, bool is_from, List *options); extern CopyState BeginCopyFrom(Relation rel, const char *filename, - List *attnamelist, List *options); + bool is_program, List *attnamelist, List *options); extern void EndCopyFrom(CopyState cstate); extern bool NextCopyFrom(CopyState cstate, ExprContext *econtext, Datum *values, bool *nulls, Oid *tupleOid); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d8678e5..a60baf7 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1407,6 +1407,7 @@ typedef struct CopyStmt List *attlist; /* List of column names (as Strings), or NIL * for all columns */ bool is_from; /* TO or FROM */ + bool is_program; /* do I execute popen/pclose? */ char *filename; /* filename, or NULL for STDIN/STDOUT */ List *options; /* List of DefElem nodes */ } CopyStmt; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 03aa761..6f67a65 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -292,6 +292,7 @@ PG_KEYWORD("prior", PRIOR, UNRESERVED_KEYWORD) PG_KEYWORD("privileges", PRIVILEGES, UNRESERVED_KEYWORD) PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD) PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD) +PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD) diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h index bd36c9d..44c28ed 100644 --- a/src/include/storage/fd.h +++ b/src/include/storage/fd.h @@ -80,6 +80,10 @@ extern char *FilePathName(File file); extern FILE *AllocateFile(const char *name, const char *mode); extern int FreeFile(FILE *file); +/* Operations that allow use of pipe stream */ +extern FILE *OpenPipeStream(const char *command, const char *mode); +extern int ClosePipeStream(FILE *file); + /* Operations to allow use of the library routines */ extern DIR *AllocateDir(const char *dirname); extern struct dirent *ReadDir(DIR *dir, const char *dirname); diff --git a/src/interfaces/ecpg/preproc/ecpg.addons b/src/interfaces/ecpg/preproc/ecpg.addons index aae3cc9..be4cc79 100644 --- a/src/interfaces/ecpg/preproc/ecpg.addons +++ b/src/interfaces/ecpg/preproc/ecpg.addons @@ -192,16 +192,36 @@ ECPG: where_or_current_clauseWHERECURRENT_POFcursor_name block char *cursor_marker = $4[0] == ':' ? mm_strdup("$0") : $4; $$ = cat_str(2,mm_strdup("where current of"), cursor_marker); } -ECPG: CopyStmtCOPYopt_binaryqualified_nameopt_column_listopt_oidscopy_fromcopy_file_namecopy_delimiteropt_withcopy_options addon - if (strcmp($6, "to") == 0 && strcmp($7, "stdin") == 0) - mmerror(PARSE_ERROR, ET_ERROR, "COPY TO STDIN is not possible"); - else if (strcmp($6, "from") == 0 && strcmp($7, "stdout") == 0) - mmerror(PARSE_ERROR, ET_ERROR, "COPY FROM STDOUT is not possible"); - else if (strcmp($6, "from") == 0 && strcmp($7, "stdin") == 0) - mmerror(PARSE_ERROR, ET_WARNING, "COPY FROM STDIN is not implemented"); -ECPG: CopyStmtCOPYselect_with_parensTOcopy_file_nameopt_withcopy_options addon - if (strcmp($4, "stdin") == 0) - mmerror(PARSE_ERROR, ET_ERROR, "COPY TO STDIN is not possible"); +ECPG: CopyStmtCOPYopt_binaryqualified_nameopt_column_listopt_oidscopy_fromcopy_programcopy_file_namecopy_delimiteropt_withcopy_options addon + if (strcmp($7, "program") == 0) + { + if (strcmp($8, "stdin") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "PROGRAM STDIN is not possible"); + else if (strcmp($8, "stdout") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "PROGRAM STDOUT is not possible"); + } + else + { + if (strcmp($6, "to") == 0 && strcmp($8, "stdin") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "COPY TO STDIN is not possible"); + else if (strcmp($6, "from") == 0 && strcmp($8, "stdout") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "COPY FROM STDOUT is not possible"); + else if (strcmp($6, "from") == 0 && strcmp($8, "stdin") == 0) + mmerror(PARSE_ERROR, ET_WARNING, "COPY FROM STDIN is not implemented"); + } +ECPG: CopyStmtCOPYselect_with_parensTOcopy_programcopy_file_nameopt_withcopy_options addon + if (strcmp($4, "program") == 0) + { + if (strcmp($5, "stdin") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "PROGRAM STDIN is not possible"); + else if (strcmp($5, "stdout") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "PROGRAM STDOUT is not possible"); + } + else + { + if (strcmp($5, "stdin") == 0) + mmerror(PARSE_ERROR, ET_ERROR, "COPY TO STDIN is not possible"); + } ECPG: var_valueNumericOnly addon if ($1[0] == '$') {