From 85188ab8838bf19cdf12298e1b6c29e12f9b9a3c Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Wed, 30 Aug 2006 23:34:22 +0000 Subject: [PATCH] Extend COPY to support COPY (SELECT ...) TO ... Bernd Helmle --- doc/src/sgml/ref/copy.sgml | 40 +- doc/src/sgml/ref/psql-ref.sgml | 9 +- src/backend/commands/copy.c | 809 +++++++++++++++-------- src/backend/nodes/copyfuncs.c | 3 +- src/backend/nodes/equalfuncs.c | 3 +- src/backend/parser/analyze.c | 15 +- src/backend/parser/gram.y | 23 +- src/backend/tcop/dest.c | 9 +- src/bin/psql/copy.c | 25 +- src/include/commands/copy.h | 5 +- src/include/nodes/parsenodes.h | 10 +- src/include/tcop/dest.h | 5 +- src/test/regress/expected/copyselect.out | 126 ++++ src/test/regress/parallel_schedule | 4 +- src/test/regress/serial_schedule | 3 +- src/test/regress/sql/copyselect.sql | 82 +++ 16 files changed, 843 insertions(+), 328 deletions(-) create mode 100644 src/test/regress/expected/copyselect.out create mode 100644 src/test/regress/sql/copyselect.sql diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index c1d87e601c2..cb1eaa08048 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -1,5 +1,5 @@ <!-- -$PostgreSQL: pgsql/doc/src/sgml/ref/copy.sgml,v 1.74 2006/04/22 03:03:11 momjian Exp $ +$PostgreSQL: pgsql/doc/src/sgml/ref/copy.sgml,v 1.75 2006/08/30 23:34:20 tgl Exp $ PostgreSQL documentation --> @@ -33,7 +33,7 @@ COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable cla [ ESCAPE [ AS ] '<replaceable class="parameter">escape</replaceable>' ] [ FORCE NOT NULL <replaceable class="parameter">column</replaceable> [, ...] ] -COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable class="parameter">column</replaceable> [, ...] ) ] +COPY { <replaceable class="parameter">tablename</replaceable> [ ( <replaceable class="parameter">column</replaceable> [, ...] ) ] | ( <replaceable class="parameter">query</replaceable> ) } TO { '<replaceable class="parameter">filename</replaceable>' | STDOUT } [ [ WITH ] [ BINARY ] @@ -57,7 +57,8 @@ COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable cla files. <command>COPY TO</command> copies the contents of a table <emphasis>to</> a file, while <command>COPY FROM</command> copies data <emphasis>from</> a file to a table (appending the data to - whatever is in the table already). + whatever is in the table already). <command>COPY TO</command> + can also copy the results of a <command>SELECT</> query. </para> <para> @@ -97,7 +98,17 @@ COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable cla <listitem> <para> An optional list of columns to be copied. If no column list is - specified, all columns will be used. + specified, all columns of the table will be copied. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">query</replaceable></term> + <listitem> + <para> + A <command>SELECT</> query whose results are to be copied. + Note that parentheses are required around the query. </para> </listitem> </varlistentry> @@ -148,7 +159,8 @@ COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable cla <para> Specifies copying the OID for each row. (An error is raised if <literal>OIDS</literal> is specified for a table that does not - have OIDs.) + have OIDs, or in the case of copying a <replaceable + class="parameter">query</replaceable>.) </para> </listitem> </varlistentry> @@ -265,7 +277,7 @@ COPY <replaceable class="parameter">tablename</replaceable> [ ( <replaceable cla COPY <replaceable class="parameter">count</replaceable> </screen> The <replaceable class="parameter">count</replaceable> is the number - of rows inserted into or copied from the table. + of rows copied. </para> </refsect1> @@ -274,7 +286,8 @@ COPY <replaceable class="parameter">count</replaceable> <para> <command>COPY</command> can only be used with plain tables, not - with views. + with views. However, you can write <literal>COPY (SELECT * FROM + <replaceable class="parameter">viewname</replaceable>) TO ...</literal>. </para> <para> @@ -320,8 +333,8 @@ COPY <replaceable class="parameter">count</replaceable> server in the case of <command>COPY TO</command>, but for <command>COPY FROM</command> you do have the option of reading from a file specified by a relative path. The path will be interpreted - relative to the working directory of the server process (somewhere below - the data directory), not the client's working directory. + relative to the working directory of the server process (normally + the cluster's data directory), not the client's working directory. </para> <para> @@ -737,14 +750,9 @@ COPY country FROM '/usr1/proj/bray/sql/country_data'; </para> <para> - To copy into a file just the countries whose names start with 'A' - using a temporary table which is automatically deleted: + To copy into a file just the countries whose names start with 'A': <programlisting> -BEGIN; -CREATE TEMP TABLE a_list_countries AS - SELECT * FROM country WHERE country_name LIKE 'A%'; -COPY a_list_countries TO '/usr1/proj/bray/sql/a_list_countries.copy'; -ROLLBACK; +COPY (SELECT * FROM country WHERE country_name LIKE 'A%') TO '/usr1/proj/bray/sql/a_list_countries.copy'; </programlisting> </para> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index d6528d0bc10..acac4d3daf3 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1,5 +1,5 @@ <!-- -$PostgreSQL: pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.167 2006/08/29 22:25:04 tgl Exp $ +$PostgreSQL: pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.168 2006/08/30 23:34:21 tgl Exp $ PostgreSQL documentation --> @@ -739,8 +739,7 @@ testdb=> </varlistentry> <varlistentry> - <term><literal>\copy <replaceable class="parameter">table</replaceable> - [ ( <replaceable class="parameter">column_list</replaceable> ) ] + <term><literal>\copy { <replaceable class="parameter">table</replaceable> [ ( <replaceable class="parameter">column_list</replaceable> ) ] | ( <replaceable class="parameter">query</replaceable> ) } { <literal>from</literal> | <literal>to</literal> } { <replaceable class="parameter">filename</replaceable> | stdin | stdout | pstdin | pstdout } [ with ] @@ -779,9 +778,7 @@ testdb=> </para> <para> - <literal>\copy <replaceable - class="parameter">table</replaceable> from <replaceable - class="parameter">stdin | stdout</replaceable></literal> + <literal>\copy ... from stdin | to stdout</literal> reads/writes based on the command input and output respectively. All rows are read from the same source that issued the command, continuing until <literal>\.</literal> is read or the stream diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 4242c1aff1c..569d86eee2b 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.268 2006/07/14 14:52:18 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.269 2006/08/30 23:34:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -31,6 +31,7 @@ #include "libpq/pqformat.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "optimizer/planner.h" #include "parser/parse_relation.h" #include "rewrite/rewriteHandler.h" #include "storage/fd.h" @@ -99,18 +100,21 @@ typedef struct CopyStateData /* parameters from the COPY command */ Relation rel; /* relation to copy to or from */ + QueryDesc *queryDesc; /* executable query to copy from */ List *attnumlist; /* integer list of attnums to copy */ + char *filename; /* filename, or NULL for STDIN/STDOUT */ bool binary; /* binary format? */ bool oids; /* include OIDs? */ bool csv_mode; /* Comma Separated Value format? */ bool header_line; /* CSV header line? */ char *null_print; /* NULL marker string (server encoding!) */ int null_print_len; /* length of same */ + char *null_print_client; /* same converted to client encoding */ char *delim; /* column delimiter (must be 1 byte) */ char *quote; /* CSV quote char (must be 1 byte) */ char *escape; /* CSV escape char (must be 1 byte) */ - List *force_quote_atts; /* integer list of attnums to FQ */ - List *force_notnull_atts; /* integer list of attnums to FNN */ + bool *force_quote_flags; /* per-column CSV FQ flags */ + bool *force_notnull_flags; /* per-column CSV FNN flags */ /* these are just for error messages, see copy_in_error_callback */ const char *cur_relname; /* table name for error messages */ @@ -118,6 +122,12 @@ typedef struct CopyStateData const char *cur_attname; /* current att for error messages */ const char *cur_attval; /* current att value for error messages */ + /* + * Working state for COPY TO + */ + FmgrInfo *out_functions; /* lookup info for output functions */ + MemoryContext rowcontext; /* per-row evaluation context */ + /* * These variables are used to reduce overhead in textual COPY FROM. * @@ -153,6 +163,13 @@ typedef struct CopyStateData typedef CopyStateData *CopyState; +/* DestReceiver for COPY (SELECT) TO */ +typedef struct +{ + DestReceiver pub; /* publicly-known function pointers */ + CopyState cstate; /* CopyStateData for the command */ +} DR_copy; + /* * These macros centralize code used to process line_buf and raw_buf buffers. @@ -225,6 +242,8 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0"; /* non-export function prototypes */ static void DoCopyTo(CopyState cstate); static void CopyTo(CopyState cstate); +static void CopyOneRowTo(CopyState cstate, Oid tupleOid, + Datum *values, bool *nulls); static void CopyFrom(CopyState cstate); static bool CopyReadLine(CopyState cstate); static bool CopyReadLineText(CopyState cstate); @@ -239,7 +258,8 @@ static Datum CopyReadBinaryAttribute(CopyState cstate, static void CopyAttributeOutText(CopyState cstate, char *string); static void CopyAttributeOutCSV(CopyState cstate, char *string, bool use_quote, bool single_attr); -static List *CopyGetAttnums(Relation rel, List *attnamelist); +static List *CopyGetAttnums(TupleDesc tupDesc, Relation rel, + List *attnamelist); static char *limit_printout_length(const char *str); /* Low-level communications functions */ @@ -668,7 +688,8 @@ CopyLoadRawBuf(CopyState cstate) * DoCopy executes the SQL COPY statement. * * Either unload or reload contents of table <relation>, depending on <from>. - * (<from> = TRUE means we are inserting into the table.) + * (<from> = TRUE means we are inserting into the table.) In the "TO" case + * we also support copying the output of an arbitrary SELECT query. * * If <pipe> is false, transfer is between the table and the file named * <filename>. Otherwise, transfer is between the table and our regular @@ -697,8 +718,6 @@ uint64 DoCopy(const CopyStmt *stmt) { CopyState cstate; - RangeVar *relation = stmt->relation; - char *filename = stmt->filename; bool is_from = stmt->is_from; bool pipe = (stmt->filename == NULL); List *attnamelist = stmt->attlist; @@ -707,6 +726,8 @@ DoCopy(const CopyStmt *stmt) AclMode required_access = (is_from ? ACL_INSERT : ACL_SELECT); AclResult aclresult; ListCell *option; + TupleDesc tupDesc; + int num_phys_attrs; uint64 processed; /* Allocate workspace and zero all fields */ @@ -920,23 +941,7 @@ DoCopy(const CopyStmt *stmt) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("CSV quote character must not appear in the NULL specification"))); - /* Open and lock the relation, using the appropriate lock type. */ - cstate->rel = heap_openrv(relation, - (is_from ? RowExclusiveLock : AccessShareLock)); - - /* check read-only transaction */ - if (XactReadOnly && is_from && - !isTempNamespace(RelationGetNamespace(cstate->rel))) - ereport(ERROR, - (errcode(ERRCODE_READ_ONLY_SQL_TRANSACTION), - errmsg("transaction is read-only"))); - - /* Check permissions. */ - aclresult = pg_class_aclcheck(RelationGetRelid(cstate->rel), GetUserId(), - required_access); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, ACL_KIND_CLASS, - RelationGetRelationName(cstate->rel)); + /* Disallow file COPY except to superusers. */ if (!pipe && !superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), @@ -944,26 +949,137 @@ DoCopy(const CopyStmt *stmt) errhint("Anyone can COPY to stdout or from stdin. " "psql's \\copy command also works for anyone."))); - /* Don't allow COPY w/ OIDs to or from a table without them */ - if (cstate->oids && !cstate->rel->rd_rel->relhasoids) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("table \"%s\" does not have OIDs", - RelationGetRelationName(cstate->rel)))); + if (stmt->relation) + { + Assert(!stmt->query); + cstate->queryDesc = NULL; + + /* Open and lock the relation, using the appropriate lock type. */ + cstate->rel = heap_openrv(stmt->relation, + (is_from ? RowExclusiveLock : AccessShareLock)); + + /* Check relation permissions. */ + aclresult = pg_class_aclcheck(RelationGetRelid(cstate->rel), + GetUserId(), + required_access); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, ACL_KIND_CLASS, + RelationGetRelationName(cstate->rel)); + + /* check read-only transaction */ + if (XactReadOnly && is_from && + !isTempNamespace(RelationGetNamespace(cstate->rel))) + ereport(ERROR, + (errcode(ERRCODE_READ_ONLY_SQL_TRANSACTION), + errmsg("transaction is read-only"))); + + /* Don't allow COPY w/ OIDs to or from a table without them */ + if (cstate->oids && !cstate->rel->rd_rel->relhasoids) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("table \"%s\" does not have OIDs", + RelationGetRelationName(cstate->rel)))); + + tupDesc = RelationGetDescr(cstate->rel); + } + else + { + Query *query = stmt->query; + List *rewritten; + Plan *plan; + DestReceiver *dest; + + Assert(query); + Assert(!is_from); + cstate->rel = NULL; + + /* Don't allow COPY w/ OIDs from a select */ + if (cstate->oids) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY (SELECT) WITH OIDS is not supported"))); + + if (query->into) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY (SELECT INTO) is not supported"))); + + /* + * The query has already been through parse analysis, but not + * rewriting or planning. Do that now. + * + * Because the planner is not cool about not scribbling on its input, + * we make a preliminary copy of the source querytree. This prevents + * problems in the case that the COPY is in a portal or plpgsql + * function and is executed repeatedly. (See also the same hack in + * EXPLAIN, DECLARE CURSOR and PREPARE.) XXX the planner really + * shouldn't modify its input ... FIXME someday. + */ + query = copyObject(query); + Assert(query->commandType == CMD_SELECT); + + /* + * Must acquire locks in case we didn't come fresh from the parser. + * XXX this also scribbles on query, another reason for copyObject + */ + AcquireRewriteLocks(query); + + /* Rewrite through rule system */ + rewritten = QueryRewrite(query); + + /* We don't expect more or less than one result query */ + if (list_length(rewritten) != 1) + elog(ERROR, "unexpected rewrite result"); + + query = (Query *) linitial(rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = planner(query, false, 0, NULL); + + /* + * Update snapshot command ID to ensure this query sees results of any + * previously executed queries. (It's a bit cheesy to modify + * ActiveSnapshot without making a copy, but for the limited ways in + * which COPY can be invoked, I think it's OK, because the active + * snapshot shouldn't be shared with anything else anyway.) + */ + ActiveSnapshot->curcid = GetCurrentCommandId(); + + /* Create dest receiver for COPY OUT */ + dest = CreateDestReceiver(DestCopyOut, NULL); + ((DR_copy *) dest)->cstate = cstate; + + /* Create a QueryDesc requesting no output */ + cstate->queryDesc = CreateQueryDesc(query, plan, + ActiveSnapshot, InvalidSnapshot, + dest, NULL, false); + + /* + * Call ExecutorStart to prepare the plan for execution. + * + * ExecutorStart computes a result tupdesc for us + */ + ExecutorStart(cstate->queryDesc, 0); + + tupDesc = cstate->queryDesc->tupDesc; + } /* Generate or convert list of attributes to process */ - cstate->attnumlist = CopyGetAttnums(cstate->rel, attnamelist); + cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); + + num_phys_attrs = tupDesc->natts; - /* Convert FORCE QUOTE name list to column numbers, check validity */ + /* Convert FORCE QUOTE name list to per-column flags, check validity */ + cstate->force_quote_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); if (force_quote) { - TupleDesc tupDesc = RelationGetDescr(cstate->rel); - Form_pg_attribute *attr = tupDesc->attrs; + List *attnums; ListCell *cur; - cstate->force_quote_atts = CopyGetAttnums(cstate->rel, force_quote); + attnums = CopyGetAttnums(tupDesc, cstate->rel, force_quote); - foreach(cur, cstate->force_quote_atts) + foreach(cur, attnums) { int attnum = lfirst_int(cur); @@ -971,21 +1087,21 @@ DoCopy(const CopyStmt *stmt) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("FORCE QUOTE column \"%s\" not referenced by COPY", - NameStr(attr[attnum - 1]->attname)))); + NameStr(tupDesc->attrs[attnum - 1]->attname)))); + cstate->force_quote_flags[attnum - 1] = true; } } - /* Convert FORCE NOT NULL name list to column numbers, check validity */ + /* Convert FORCE NOT NULL name list to per-column flags, check validity */ + cstate->force_notnull_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); if (force_notnull) { - TupleDesc tupDesc = RelationGetDescr(cstate->rel); - Form_pg_attribute *attr = tupDesc->attrs; + List *attnums; ListCell *cur; - cstate->force_notnull_atts = CopyGetAttnums(cstate->rel, - force_notnull); + attnums = CopyGetAttnums(tupDesc, cstate->rel, force_notnull); - foreach(cur, cstate->force_notnull_atts) + foreach(cur, attnums) { int attnum = lfirst_int(cur); @@ -993,7 +1109,8 @@ DoCopy(const CopyStmt *stmt) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("FORCE NOT NULL column \"%s\" not referenced by COPY", - NameStr(attr[attnum - 1]->attname)))); + NameStr(tupDesc->attrs[attnum - 1]->attname)))); + cstate->force_notnull_flags[attnum - 1] = true; } } @@ -1018,67 +1135,59 @@ DoCopy(const CopyStmt *stmt) cstate->encoding_embeds_ascii = PG_ENCODING_IS_CLIENT_ONLY(cstate->client_encoding); cstate->copy_dest = COPY_FILE; /* default */ + cstate->filename = stmt->filename; - if (is_from) - { /* copy from file to database */ - if (cstate->rel->rd_rel->relkind != RELKIND_RELATION) - { - if (cstate->rel->rd_rel->relkind == RELKIND_VIEW) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot copy to view \"%s\"", - RelationGetRelationName(cstate->rel)))); - else if (cstate->rel->rd_rel->relkind == RELKIND_SEQUENCE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot copy to sequence \"%s\"", - RelationGetRelationName(cstate->rel)))); - else - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot copy to non-table relation \"%s\"", - RelationGetRelationName(cstate->rel)))); - } - if (pipe) - { - if (whereToSendOutput == DestRemote) - ReceiveCopyBegin(cstate); - else - cstate->copy_file = stdin; - } - else - { - struct stat st; + if (is_from) /* copy from file to database */ + CopyFrom(cstate); + else /* copy from database to file */ + DoCopyTo(cstate); - cstate->copy_file = AllocateFile(filename, PG_BINARY_R); + /* + * Close the relation or query. If reading, we can release the + * AccessShareLock we got; if writing, we should hold the lock until end + * of transaction to ensure that updates will be committed before lock is + * released. + */ + if (cstate->rel) + heap_close(cstate->rel, (is_from ? NoLock : AccessShareLock)); + else + { + /* Close down the query and free resources. */ + ExecutorEnd(cstate->queryDesc); + FreeQueryDesc(cstate->queryDesc); + } - if (cstate->copy_file == NULL) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not open file \"%s\" for reading: %m", - filename))); + /* Clean up storage (probably not really necessary) */ + processed = cstate->processed; - fstat(fileno(cstate->copy_file), &st); - if (S_ISDIR(st.st_mode)) - { - FreeFile(cstate->copy_file); - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a directory", filename))); - } - } + pfree(cstate->attribute_buf.data); + pfree(cstate->line_buf.data); + pfree(cstate->raw_buf); + pfree(cstate); - CopyFrom(cstate); - } - else - { /* copy from database to file */ + return processed; +} + + +/* + * This intermediate routine exists mainly to localize the effects of setjmp + * so we don't need to plaster a lot of variables with "volatile". + */ +static void +DoCopyTo(CopyState cstate) +{ + bool pipe = (cstate->filename == NULL); + + if (cstate->rel) + { if (cstate->rel->rd_rel->relkind != RELKIND_RELATION) { if (cstate->rel->rd_rel->relkind == RELKIND_VIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot copy from view \"%s\"", - RelationGetRelationName(cstate->rel)))); + RelationGetRelationName(cstate->rel)), + errhint("Try the COPY (SELECT ...) TO variant."))); else if (cstate->rel->rd_rel->relkind == RELKIND_SEQUENCE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -1090,86 +1199,49 @@ DoCopy(const CopyStmt *stmt) errmsg("cannot copy from non-table relation \"%s\"", RelationGetRelationName(cstate->rel)))); } - if (pipe) - { - if (whereToSendOutput == DestRemote) - cstate->fe_copy = true; - else - cstate->copy_file = stdout; - } - else - { - mode_t oumask; /* Pre-existing umask value */ - struct stat st; - - /* - * 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"))); - - oumask = umask((mode_t) 022); - cstate->copy_file = AllocateFile(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", - filename))); - - fstat(fileno(cstate->copy_file), &st); - if (S_ISDIR(st.st_mode)) - { - FreeFile(cstate->copy_file); - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a directory", filename))); - } - } - - DoCopyTo(cstate); } - if (!pipe) + if (pipe) { - /* we assume only the write case could fail here */ - if (FreeFile(cstate->copy_file)) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not write to file \"%s\": %m", - filename))); + if (whereToSendOutput == DestRemote) + cstate->fe_copy = true; + else + cstate->copy_file = stdout; } + else + { + mode_t oumask; /* Pre-existing umask value */ + struct stat st; - /* - * Close the relation. If reading, we can release the AccessShareLock we - * got; if writing, we should hold the lock until end of transaction to - * ensure that updates will be committed before lock is released. - */ - heap_close(cstate->rel, (is_from ? NoLock : AccessShareLock)); - - /* Clean up storage (probably not really necessary) */ - processed = cstate->processed; + /* + * Prevent write to relative path ... too easy to shoot oneself in + * the foot by overwriting a database file ... + */ + if (!is_absolute_path(cstate->filename)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("relative path not allowed for COPY to file"))); - pfree(cstate->attribute_buf.data); - pfree(cstate->line_buf.data); - pfree(cstate->raw_buf); - pfree(cstate); + oumask = umask((mode_t) 022); + cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_W); + umask(oumask); - return processed; -} + 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)) + { + FreeFile(cstate->copy_file); + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a directory", cstate->filename))); + } + } -/* - * This intermediate routine just exists to localize the effects of setjmp - * so we don't need to plaster a lot of variables with "volatile". - */ -static void -DoCopyTo(CopyState cstate) -{ PG_TRY(); { if (cstate->fe_copy) @@ -1191,40 +1263,41 @@ DoCopyTo(CopyState cstate) PG_RE_THROW(); } PG_END_TRY(); + + if (!pipe) + { + if (FreeFile(cstate->copy_file)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", + cstate->filename))); + } } /* - * Copy from relation TO file. + * Copy from relation or query TO file. */ static void CopyTo(CopyState cstate) { - HeapTuple tuple; TupleDesc tupDesc; - HeapScanDesc scandesc; int num_phys_attrs; - int attr_count; Form_pg_attribute *attr; - FmgrInfo *out_functions; - bool *force_quote; - char *string; - char *null_print_client; ListCell *cur; - MemoryContext oldcontext; - MemoryContext mycontext; - tupDesc = cstate->rel->rd_att; + if (cstate->rel) + tupDesc = RelationGetDescr(cstate->rel); + else + tupDesc = cstate->queryDesc->tupDesc; attr = tupDesc->attrs; num_phys_attrs = tupDesc->natts; - attr_count = list_length(cstate->attnumlist); - null_print_client = cstate->null_print; /* default */ + cstate->null_print_client = cstate->null_print; /* default */ /* We use fe_msgbuf as a per-row buffer regardless of copy_dest */ cstate->fe_msgbuf = makeStringInfo(); /* Get info about the columns we need to process. */ - out_functions = (FmgrInfo *) palloc(num_phys_attrs * sizeof(FmgrInfo)); - force_quote = (bool *) palloc(num_phys_attrs * sizeof(bool)); + cstate->out_functions = (FmgrInfo *) palloc(num_phys_attrs * sizeof(FmgrInfo)); foreach(cur, cstate->attnumlist) { int attnum = lfirst_int(cur); @@ -1239,12 +1312,7 @@ CopyTo(CopyState cstate) getTypeOutputInfo(attr[attnum - 1]->atttypid, &out_func_oid, &isvarlena); - fmgr_info(out_func_oid, &out_functions[attnum - 1]); - - if (list_member_int(cstate->force_quote_atts, attnum)) - force_quote[attnum - 1] = true; - else - force_quote[attnum - 1] = false; + fmgr_info(out_func_oid, &cstate->out_functions[attnum - 1]); } /* @@ -1253,11 +1321,11 @@ CopyTo(CopyState cstate) * datatype output routines, and should be faster than retail pfree's * anyway. (We don't need a whole econtext as CopyFrom does.) */ - mycontext = AllocSetContextCreate(CurrentMemoryContext, - "COPY TO", - ALLOCSET_DEFAULT_MINSIZE, - ALLOCSET_DEFAULT_INITSIZE, - ALLOCSET_DEFAULT_MAXSIZE); + cstate->rowcontext = AllocSetContextCreate(CurrentMemoryContext, + "COPY TO", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); if (cstate->binary) { @@ -1282,7 +1350,7 @@ CopyTo(CopyState cstate) * encoding, because it will be sent directly with CopySendString. */ if (cstate->need_transcoding) - null_print_client = pg_server_to_client(cstate->null_print, + cstate->null_print_client = pg_server_to_client(cstate->null_print, cstate->null_print_len); /* if a header has been requested send the line */ @@ -1309,113 +1377,139 @@ CopyTo(CopyState cstate) } } - scandesc = heap_beginscan(cstate->rel, ActiveSnapshot, 0, NULL); - - while ((tuple = heap_getnext(scandesc, ForwardScanDirection)) != NULL) + if (cstate->rel) { - bool need_delim = false; + Datum *values; + bool *nulls; + HeapScanDesc scandesc; + HeapTuple tuple; - CHECK_FOR_INTERRUPTS(); + values = (Datum *) palloc(num_phys_attrs * sizeof(Datum)); + nulls = (bool *) palloc(num_phys_attrs * sizeof(bool)); - MemoryContextReset(mycontext); - oldcontext = MemoryContextSwitchTo(mycontext); + scandesc = heap_beginscan(cstate->rel, ActiveSnapshot, 0, NULL); - if (cstate->binary) + while ((tuple = heap_getnext(scandesc, ForwardScanDirection)) != NULL) { - /* Binary per-tuple header */ - CopySendInt16(cstate, attr_count); - /* Send OID if wanted --- note attr_count doesn't include it */ - if (cstate->oids) - { - Oid oid = HeapTupleGetOid(tuple); + CHECK_FOR_INTERRUPTS(); - /* Hack --- assume Oid is same size as int32 */ - CopySendInt32(cstate, sizeof(int32)); - CopySendInt32(cstate, oid); - } + /* Deconstruct the tuple ... faster than repeated heap_getattr */ + heap_deform_tuple(tuple, tupDesc, values, nulls); + + /* Format and send the data */ + CopyOneRowTo(cstate, HeapTupleGetOid(tuple), values, nulls); } - else + + heap_endscan(scandesc); + } + else + { + /* run the plan --- the dest receiver will send tuples */ + ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L); + } + + if (cstate->binary) + { + /* Generate trailer for a binary copy */ + CopySendInt16(cstate, -1); + /* Need to flush out the trailer */ + CopySendEndOfRow(cstate); + } + + MemoryContextDelete(cstate->rowcontext); +} + +/* + * Emit one row during CopyTo(). + */ +static void +CopyOneRowTo(CopyState cstate, Oid tupleOid, Datum *values, bool *nulls) +{ + bool need_delim = false; + FmgrInfo *out_functions = cstate->out_functions; + MemoryContext oldcontext; + ListCell *cur; + char *string; + + MemoryContextReset(cstate->rowcontext); + oldcontext = MemoryContextSwitchTo(cstate->rowcontext); + + if (cstate->binary) + { + /* Binary per-tuple header */ + CopySendInt16(cstate, list_length(cstate->attnumlist)); + /* Send OID if wanted --- note attnumlist doesn't include it */ + if (cstate->oids) { - /* Text format has no per-tuple header, but send OID if wanted */ - /* Assume digits don't need any quoting or encoding conversion */ - if (cstate->oids) - { - string = DatumGetCString(DirectFunctionCall1(oidout, - ObjectIdGetDatum(HeapTupleGetOid(tuple)))); - CopySendString(cstate, string); - need_delim = true; - } + /* Hack --- assume Oid is same size as int32 */ + CopySendInt32(cstate, sizeof(int32)); + CopySendInt32(cstate, tupleOid); } - - foreach(cur, cstate->attnumlist) + } + else + { + /* Text format has no per-tuple header, but send OID if wanted */ + /* Assume digits don't need any quoting or encoding conversion */ + if (cstate->oids) { - int attnum = lfirst_int(cur); - Datum value; - bool isnull; + string = DatumGetCString(DirectFunctionCall1(oidout, + ObjectIdGetDatum(tupleOid))); + CopySendString(cstate, string); + need_delim = true; + } + } + + foreach(cur, cstate->attnumlist) + { + int attnum = lfirst_int(cur); + Datum value = values[attnum - 1]; + bool isnull = nulls[attnum - 1]; - value = heap_getattr(tuple, attnum, tupDesc, &isnull); + if (!cstate->binary) + { + if (need_delim) + CopySendChar(cstate, cstate->delim[0]); + need_delim = true; + } + if (isnull) + { + if (!cstate->binary) + CopySendString(cstate, cstate->null_print_client); + else + CopySendInt32(cstate, -1); + } + else + { if (!cstate->binary) { - if (need_delim) - CopySendChar(cstate, cstate->delim[0]); - need_delim = true; - } - - if (isnull) - { - if (!cstate->binary) - CopySendString(cstate, null_print_client); + string = OutputFunctionCall(&out_functions[attnum - 1], + value); + if (cstate->csv_mode) + CopyAttributeOutCSV(cstate, string, + cstate->force_quote_flags[attnum - 1], + list_length(cstate->attnumlist) == 1); else - CopySendInt32(cstate, -1); + CopyAttributeOutText(cstate, string); } else { - if (!cstate->binary) - { - string = OutputFunctionCall(&out_functions[attnum - 1], - value); - if (cstate->csv_mode) - CopyAttributeOutCSV(cstate, string, - force_quote[attnum - 1], - list_length(cstate->attnumlist) == 1); - else - CopyAttributeOutText(cstate, string); - } - else - { - bytea *outputbytes; + bytea *outputbytes; - outputbytes = SendFunctionCall(&out_functions[attnum - 1], - value); - CopySendInt32(cstate, VARSIZE(outputbytes) - VARHDRSZ); - CopySendData(cstate, VARDATA(outputbytes), - VARSIZE(outputbytes) - VARHDRSZ); - } + outputbytes = SendFunctionCall(&out_functions[attnum - 1], + value); + CopySendInt32(cstate, VARSIZE(outputbytes) - VARHDRSZ); + CopySendData(cstate, VARDATA(outputbytes), + VARSIZE(outputbytes) - VARHDRSZ); } } - - CopySendEndOfRow(cstate); - - MemoryContextSwitchTo(oldcontext); - - cstate->processed++; - } - - heap_endscan(scandesc); - - if (cstate->binary) - { - /* Generate trailer for a binary copy */ - CopySendInt16(cstate, -1); - /* Need to flush out the trailer */ - CopySendEndOfRow(cstate); } - MemoryContextDelete(mycontext); + CopySendEndOfRow(cstate); - pfree(out_functions); - pfree(force_quote); + MemoryContextSwitchTo(oldcontext); + + cstate->processed++; } @@ -1528,6 +1622,7 @@ limit_printout_length(const char *str) static void CopyFrom(CopyState cstate) { + bool pipe = (cstate->filename == NULL); HeapTuple tuple; TupleDesc tupDesc; Form_pg_attribute *attr; @@ -1538,7 +1633,6 @@ CopyFrom(CopyState cstate) FmgrInfo oid_in_function; Oid *typioparams; Oid oid_typioparam; - bool *force_notnull; int attnum; int i; Oid in_func_oid; @@ -1558,6 +1652,56 @@ CopyFrom(CopyState cstate) MemoryContext oldcontext = CurrentMemoryContext; ErrorContextCallback errcontext; + Assert(cstate->rel); + + if (cstate->rel->rd_rel->relkind != RELKIND_RELATION) + { + if (cstate->rel->rd_rel->relkind == RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy to view \"%s\"", + RelationGetRelationName(cstate->rel)))); + else if (cstate->rel->rd_rel->relkind == RELKIND_SEQUENCE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy to sequence \"%s\"", + RelationGetRelationName(cstate->rel)))); + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy to non-table relation \"%s\"", + RelationGetRelationName(cstate->rel)))); + } + + if (pipe) + { + if (whereToSendOutput == DestRemote) + ReceiveCopyBegin(cstate); + else + cstate->copy_file = stdin; + } + else + { + struct stat st; + + 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)) + { + FreeFile(cstate->copy_file); + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a directory", cstate->filename))); + } + } + tupDesc = RelationGetDescr(cstate->rel); attr = tupDesc->attrs; num_phys_attrs = tupDesc->natts; @@ -1599,7 +1743,6 @@ CopyFrom(CopyState cstate) typioparams = (Oid *) palloc(num_phys_attrs * sizeof(Oid)); defmap = (int *) palloc(num_phys_attrs * sizeof(int)); defexprs = (ExprState **) palloc(num_phys_attrs * sizeof(ExprState *)); - force_notnull = (bool *) palloc(num_phys_attrs * sizeof(bool)); for (attnum = 1; attnum <= num_phys_attrs; attnum++) { @@ -1616,11 +1759,6 @@ CopyFrom(CopyState cstate) &in_func_oid, &typioparams[attnum - 1]); fmgr_info(in_func_oid, &in_functions[attnum - 1]); - if (list_member_int(cstate->force_notnull_atts, attnum)) - force_notnull[attnum - 1] = true; - else - force_notnull[attnum - 1] = false; - /* Get default info if needed */ if (!list_member_int(cstate->attnumlist, attnum)) { @@ -1810,7 +1948,8 @@ CopyFrom(CopyState cstate) NameStr(attr[m]->attname)))); string = field_strings[fieldno++]; - if (cstate->csv_mode && string == NULL && force_notnull[m]) + if (cstate->csv_mode && string == NULL && + cstate->force_notnull_flags[m]) { /* Go ahead and read the NULL string */ string = cstate->null_print; @@ -1972,13 +2111,21 @@ CopyFrom(CopyState cstate) pfree(typioparams); pfree(defmap); pfree(defexprs); - pfree(force_notnull); ExecDropSingleTupleTableSlot(slot); ExecCloseIndices(resultRelInfo); FreeExecutorState(estate); + + if (!pipe) + { + if (FreeFile(cstate->copy_file)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read from file \"%s\": %m", + cstate->filename))); + } } @@ -3055,16 +3202,17 @@ CopyAttributeOutCSV(CopyState cstate, char *string, * The input attnamelist is either the user-specified column list, * or NIL if there was none (in which case we want all the non-dropped * columns). + * + * rel can be NULL ... it's only used for error reports. */ static List * -CopyGetAttnums(Relation rel, List *attnamelist) +CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist) { List *attnums = NIL; if (attnamelist == NIL) { /* Generate default column list */ - TupleDesc tupDesc = RelationGetDescr(rel); Form_pg_attribute *attr = tupDesc->attrs; int attr_count = tupDesc->natts; int i; @@ -3085,15 +3233,33 @@ CopyGetAttnums(Relation rel, List *attnamelist) { char *name = strVal(lfirst(l)); int attnum; + int i; /* Lookup column name */ - /* Note we disallow system columns here */ - attnum = attnameAttNum(rel, name, false); + attnum = InvalidAttrNumber; + for (i = 0; i < tupDesc->natts; i++) + { + if (tupDesc->attrs[i]->attisdropped) + continue; + if (namestrcmp(&(tupDesc->attrs[i]->attname), name) == 0) + { + attnum = tupDesc->attrs[i]->attnum; + break; + } + } if (attnum == InvalidAttrNumber) - ereport(ERROR, + { + if (rel != NULL) + ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" of relation \"%s\" does not exist", - name, RelationGetRelationName(rel)))); + name, RelationGetRelationName(rel)))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" does not exist", + name))); + } /* Check for duplicates */ if (list_member_int(attnums, attnum)) ereport(ERROR, @@ -3106,3 +3272,66 @@ CopyGetAttnums(Relation rel, List *attnamelist) return attnums; } + + +/* + * copy_dest_startup --- executor startup + */ +static void +copy_dest_startup(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + /* no-op */ +} + +/* + * copy_dest_receive --- receive one tuple + */ +static void +copy_dest_receive(TupleTableSlot *slot, DestReceiver *self) +{ + DR_copy *myState = (DR_copy *) self; + CopyState cstate = myState->cstate; + + /* Make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + /* And send the data */ + CopyOneRowTo(cstate, InvalidOid, slot->tts_values, slot->tts_isnull); +} + +/* + * copy_dest_shutdown --- executor end + */ +static void +copy_dest_shutdown(DestReceiver *self) +{ + /* no-op */ +} + +/* + * copy_dest_destroy --- release DestReceiver object + */ +static void +copy_dest_destroy(DestReceiver *self) +{ + pfree(self); +} + +/* + * CreateCopyDestReceiver -- create a suitable DestReceiver object + */ +DestReceiver * +CreateCopyDestReceiver(void) +{ + DR_copy *self = (DR_copy *) palloc(sizeof(DR_copy)); + + self->pub.receiveSlot = copy_dest_receive; + self->pub.rStartup = copy_dest_startup; + self->pub.rShutdown = copy_dest_shutdown; + self->pub.rDestroy = copy_dest_destroy; + self->pub.mydest = DestCopyOut; + + self->cstate = NULL; /* will be set later */ + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 391846bee2c..fb1037f1704 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -15,7 +15,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.349 2006/08/25 04:06:49 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.350 2006/08/30 23:34:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1934,6 +1934,7 @@ _copyCopyStmt(CopyStmt *from) CopyStmt *newnode = makeNode(CopyStmt); COPY_NODE_FIELD(relation); + COPY_NODE_FIELD(query); COPY_NODE_FIELD(attlist); COPY_SCALAR_FIELD(is_from); COPY_STRING_FIELD(filename); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 3cb4b8aee31..1912cdd319a 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -18,7 +18,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.283 2006/08/25 04:06:49 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.284 2006/08/30 23:34:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -863,6 +863,7 @@ static bool _equalCopyStmt(CopyStmt *a, CopyStmt *b) { COMPARE_NODE_FIELD(relation); + COMPARE_NODE_FIELD(query); COMPARE_NODE_FIELD(attlist); COMPARE_SCALAR_FIELD(is_from); COMPARE_STRING_FIELD(filename); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index ae3469c86c7..23e956798d6 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.348 2006/08/25 04:06:51 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/analyze.c,v 1.349 2006/08/30 23:34:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -341,6 +341,19 @@ transformStmt(ParseState *pstate, Node *parseTree, } break; + case T_CopyStmt: + { + CopyStmt *n = (CopyStmt *) parseTree; + + result = makeNode(Query); + result->commandType = CMD_UTILITY; + if (n->query) + n->query = transformStmt(pstate, (Node *) n->query, + extras_before, extras_after); + result->utilityStmt = (Node *) parseTree; + } + break; + case T_AlterTableStmt: result = transformAlterTableStmt(pstate, (AlterTableStmt *) parseTree, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a77e73a43fc..4a0ce515b8a 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11,7 +11,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.558 2006/08/25 04:06:51 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.559 2006/08/30 23:34:21 tgl Exp $ * * HISTORY * AUTHOR DATE MAJOR EVENT @@ -1614,11 +1614,15 @@ ClosePortalStmt: /***************************************************************************** * * QUERY : - * COPY <relname> ['(' columnList ')'] FROM/TO [WITH options] + * COPY relname ['(' columnList ')'] FROM/TO file [WITH options] * * BINARY, OIDS, and DELIMITERS kept in old locations * for backward compatibility. 2002-06-18 * + * COPY ( SELECT ... ) TO file [WITH options] + * This form doesn't have the backwards-compatible option + * syntax. + * *****************************************************************************/ CopyStmt: COPY opt_binary qualified_name opt_column_list opt_oids @@ -1626,6 +1630,7 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list opt_oids { CopyStmt *n = makeNode(CopyStmt); n->relation = $3; + n->query = NULL; n->attlist = $4; n->is_from = $6; n->filename = $7; @@ -1642,6 +1647,18 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list opt_oids n->options = list_concat(n->options, $10); $$ = (Node *)n; } + | COPY select_with_parens TO copy_file_name opt_with + copy_opt_list + { + CopyStmt *n = makeNode(CopyStmt); + n->relation = NULL; + n->query = (Query *) $2; + n->attlist = NIL; + n->is_from = false; + n->filename = $4; + n->options = $6; + $$ = (Node *)n; + } ; copy_from: @@ -1652,7 +1669,7 @@ copy_from: /* * 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 - * stdout. We silently correct the "typo". - AY 9/94 + * stdout. We silently correct the "typo".) - AY 9/94 */ copy_file_name: Sconst { $$ = $1; } diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index fe7115b5f02..8276485834e 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/tcop/dest.c,v 1.69 2006/08/12 02:52:05 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/tcop/dest.c,v 1.70 2006/08/30 23:34:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -30,6 +30,7 @@ #include "access/printtup.h" #include "access/xact.h" +#include "commands/copy.h" #include "executor/executor.h" #include "executor/tstoreReceiver.h" #include "libpq/libpq.h" @@ -128,6 +129,9 @@ CreateDestReceiver(CommandDest dest, Portal portal) case DestIntoRel: return CreateIntoRelDestReceiver(); + + case DestCopyOut: + return CreateCopyDestReceiver(); } /* should never get here */ @@ -153,6 +157,7 @@ EndCommand(const char *commandTag, CommandDest dest) case DestSPI: case DestTuplestore: case DestIntoRel: + case DestCopyOut: break; } } @@ -192,6 +197,7 @@ NullCommand(CommandDest dest) case DestSPI: case DestTuplestore: case DestIntoRel: + case DestCopyOut: break; } } @@ -233,6 +239,7 @@ ReadyForQuery(CommandDest dest) case DestSPI: case DestTuplestore: case DestIntoRel: + case DestCopyOut: break; } } diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index 0d7cb5b4e00..6e514c1c064 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -3,7 +3,7 @@ * * Copyright (c) 2000-2006, PostgreSQL Global Development Group * - * $PostgreSQL: pgsql/src/bin/psql/copy.c,v 1.67 2006/08/29 15:19:50 tgl Exp $ + * $PostgreSQL: pgsql/src/bin/psql/copy.c,v 1.68 2006/08/30 23:34:22 tgl Exp $ */ #include "postgres_fe.h" #include "copy.h" @@ -39,6 +39,9 @@ * \copy tablename [(columnlist)] from|to filename * [ with ] [ binary ] [ oids ] [ delimiter [as] char ] [ null [as] string ] * + * \copy ( select stmt ) to filename + * [ with ] [ binary ] [ delimiter [as] char ] [ null [as] string ] + * * The pre-7.3 syntax was: * \copy [ binary ] tablename [(columnlist)] [with oids] from|to filename * [ [using] delimiters char ] [ with null as string ] @@ -142,6 +145,26 @@ parse_slash_copy(const char *args) result->table = pg_strdup(token); + /* Handle COPY (SELECT) case */ + if (token[0] == '(') + { + int parens = 1; + + while (parens > 0) + { + token = strtokx(NULL, whitespace, ".,()", "\"'", + nonstd_backslash, true, false, pset.encoding); + if (!token) + goto error; + if (token[0] == '(') + parens++; + else if (token[0] == ')') + parens--; + xstrcat(&result->table, " "); + xstrcat(&result->table, token); + } + } + token = strtokx(NULL, whitespace, ".,()", "\"", 0, false, false, pset.encoding); if (!token) diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index 60ddd8c92c2..ec328576c3f 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/commands/copy.h,v 1.27 2006/03/05 15:58:55 momjian Exp $ + * $PostgreSQL: pgsql/src/include/commands/copy.h,v 1.28 2006/08/30 23:34:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -15,8 +15,11 @@ #define COPY_H #include "nodes/parsenodes.h" +#include "tcop/dest.h" extern uint64 DoCopy(const CopyStmt *stmt); +extern DestReceiver *CreateCopyDestReceiver(void); + #endif /* COPY_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7aa7bfd38e0..9e808396734 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.325 2006/08/25 04:06:56 tgl Exp $ + * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.326 2006/08/30 23:34:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1012,16 +1012,22 @@ typedef struct GrantRoleStmt /* ---------------------- * Copy Statement + * + * We support "COPY relation FROM file", "COPY relation TO file", and + * "COPY (query) TO file". In any given CopyStmt, exactly one of "relation" + * and "query" must be non-NULL. Note: "query" is a SelectStmt before + * parse analysis, and a Query afterwards. * ---------------------- */ typedef struct CopyStmt { NodeTag type; RangeVar *relation; /* the relation to copy */ + Query *query; /* the query to copy */ List *attlist; /* List of column names (as Strings), or NIL * for all columns */ bool is_from; /* TO or FROM */ - char *filename; /* if NULL, use stdin/stdout */ + char *filename; /* filename, or NULL for STDIN/STDOUT */ List *options; /* List of DefElem nodes */ } CopyStmt; diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 0e0c640d2ac..0903f229ddc 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -54,7 +54,7 @@ * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/tcop/dest.h,v 1.51 2006/08/12 02:52:06 tgl Exp $ + * $PostgreSQL: pgsql/src/include/tcop/dest.h,v 1.52 2006/08/30 23:34:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -85,7 +85,8 @@ typedef enum DestRemoteExecute, /* sent to frontend, in Execute command */ DestSPI, /* results sent to SPI manager */ DestTuplestore, /* results sent to Tuplestore */ - DestIntoRel /* results sent to relation (SELECT INTO) */ + DestIntoRel, /* results sent to relation (SELECT INTO) */ + DestCopyOut /* results sent to COPY TO code */ } CommandDest; /* ---------------- diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out new file mode 100644 index 00000000000..c42bad143e4 --- /dev/null +++ b/src/test/regress/expected/copyselect.out @@ -0,0 +1,126 @@ +-- +-- Test cases for COPY (select) TO +-- +create table test1 (id serial, t text); +NOTICE: CREATE TABLE will create implicit sequence "test1_id_seq" for serial column "test1.id" +insert into test1 (t) values ('a'); +insert into test1 (t) values ('b'); +insert into test1 (t) values ('c'); +insert into test1 (t) values ('d'); +insert into test1 (t) values ('e'); +create table test2 (id serial, t text); +NOTICE: CREATE TABLE will create implicit sequence "test2_id_seq" for serial column "test2.id" +insert into test2 (t) values ('A'); +insert into test2 (t) values ('B'); +insert into test2 (t) values ('C'); +insert into test2 (t) values ('D'); +insert into test2 (t) values ('E'); +create view v_test1 +as select 'v_'||t from test1; +-- +-- Test COPY table TO +-- +copy test1 to stdout; +1 a +2 b +3 c +4 d +5 e +-- +-- This should fail +-- +copy v_test1 to stdout; +ERROR: cannot copy from view "v_test1" +HINT: Try the COPY (SELECT ...) TO variant. +-- +-- Test COPY (select) TO +-- +copy (select t from test1 where id=1) to stdout; +a +-- +-- Test COPY (select for update) TO +-- +copy (select t from test1 where id=3 for update) to stdout; +c +-- +-- This should fail +-- +copy (select t into temp test3 from test1 where id=3) to stdout; +ERROR: COPY (SELECT INTO) is not supported +-- +-- This should fail +-- +copy (select * from test1) from stdin; +ERROR: syntax error at or near "from" +LINE 1: copy (select * from test1) from stdin; + ^ +-- +-- This should fail +-- +copy (select * from test1) (t,id) to stdout; +ERROR: syntax error at or near "(" +LINE 1: copy (select * from test1) (t,id) to stdout; + ^ +-- +-- Test JOIN +-- +copy (select * from test1 join test2 using (id)) to stdout; +1 a A +2 b B +3 c C +4 d D +5 e E +-- +-- Test UNION SELECT +-- +copy (select t from test1 where id = 1 UNION select * from v_test1) to stdout; +a +v_a +v_b +v_c +v_d +v_e +-- +-- Test subselect +-- +copy (select * from (select t from test1 where id = 1 UNION select * from v_test1) t1) to stdout; +a +v_a +v_b +v_c +v_d +v_e +-- +-- Test headers, CSV and quotes +-- +copy (select t from test1 where id = 1) to stdout csv header force quote t; +t +"a" +-- +-- Test psql builtins, plain table +-- +\copy test1 to stdout +1 a +2 b +3 c +4 d +5 e +-- +-- This should fail +-- +\copy v_test1 to stdout +ERROR: cannot copy from view "v_test1" +HINT: Try the COPY (SELECT ...) TO variant. +\copy: ERROR: cannot copy from view "v_test1" +HINT: Try the COPY (SELECT ...) TO variant. +-- +-- Test \copy (select ...) +-- +\copy (select "id",'id','id""'||t,(id + 1)*id,t,"test1"."t" from test1 where id=3) to stdout +3 id id""c 12 c c +-- +-- Drop everything +-- +drop table test2; +drop view v_test1; +drop table test1; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index d675c07ff18..f13ea4792a3 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -1,6 +1,6 @@ # ---------- # The first group of parallel test -# $PostgreSQL: pgsql/src/test/regress/parallel_schedule,v 1.34 2006/08/12 02:52:06 tgl Exp $ +# $PostgreSQL: pgsql/src/test/regress/parallel_schedule,v 1.35 2006/08/30 23:34:22 tgl Exp $ # ---------- test: boolean char name varchar text int2 int4 int8 oid float4 float8 bit numeric @@ -34,7 +34,7 @@ test: create_function_2 # execute two copy tests parallel, to check that copy itself # is concurrent safe. # ---------- -test: copy +test: copy copyselect # ---------- # The third group of parallel test diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 1bb8742da6c..2d44e585d38 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -1,4 +1,4 @@ -# $PostgreSQL: pgsql/src/test/regress/serial_schedule,v 1.32 2006/08/12 02:52:06 tgl Exp $ +# $PostgreSQL: pgsql/src/test/regress/serial_schedule,v 1.33 2006/08/30 23:34:22 tgl Exp $ # This should probably be in an order similar to parallel_schedule. test: boolean test: char @@ -43,6 +43,7 @@ test: create_type test: create_table test: create_function_2 test: copy +test: copyselect test: constraints test: triggers test: create_misc diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql new file mode 100644 index 00000000000..c2526487c8c --- /dev/null +++ b/src/test/regress/sql/copyselect.sql @@ -0,0 +1,82 @@ +-- +-- Test cases for COPY (select) TO +-- +create table test1 (id serial, t text); +insert into test1 (t) values ('a'); +insert into test1 (t) values ('b'); +insert into test1 (t) values ('c'); +insert into test1 (t) values ('d'); +insert into test1 (t) values ('e'); + +create table test2 (id serial, t text); +insert into test2 (t) values ('A'); +insert into test2 (t) values ('B'); +insert into test2 (t) values ('C'); +insert into test2 (t) values ('D'); +insert into test2 (t) values ('E'); + +create view v_test1 +as select 'v_'||t from test1; + +-- +-- Test COPY table TO +-- +copy test1 to stdout; +-- +-- This should fail +-- +copy v_test1 to stdout; +-- +-- Test COPY (select) TO +-- +copy (select t from test1 where id=1) to stdout; +-- +-- Test COPY (select for update) TO +-- +copy (select t from test1 where id=3 for update) to stdout; +-- +-- This should fail +-- +copy (select t into temp test3 from test1 where id=3) to stdout; +-- +-- This should fail +-- +copy (select * from test1) from stdin; +-- +-- This should fail +-- +copy (select * from test1) (t,id) to stdout; +-- +-- Test JOIN +-- +copy (select * from test1 join test2 using (id)) to stdout; +-- +-- Test UNION SELECT +-- +copy (select t from test1 where id = 1 UNION select * from v_test1) to stdout; +-- +-- Test subselect +-- +copy (select * from (select t from test1 where id = 1 UNION select * from v_test1) t1) to stdout; +-- +-- Test headers, CSV and quotes +-- +copy (select t from test1 where id = 1) to stdout csv header force quote t; +-- +-- Test psql builtins, plain table +-- +\copy test1 to stdout +-- +-- This should fail +-- +\copy v_test1 to stdout +-- +-- Test \copy (select ...) +-- +\copy (select "id",'id','id""'||t,(id + 1)*id,t,"test1"."t" from test1 where id=3) to stdout +-- +-- Drop everything +-- +drop table test2; +drop view v_test1; +drop table test1; -- GitLab