Remove a couple hundred lines of ugly and tedious-to-maintain code by not

trying to parse COPY options exactly in psql's \copy support.  Instead,
just send the options as-is and let the backend sort it out.

Emmanuel Cecchet
This commit is contained in:
Tom Lane 2009-09-19 21:51:21 +00:00
parent 9dcc9c6b3b
commit 94f238cf1a
1 changed files with 47 additions and 270 deletions

View File

@ -3,7 +3,7 @@
* *
* Copyright (c) 2000-2009, PostgreSQL Global Development Group * Copyright (c) 2000-2009, PostgreSQL Global Development Group
* *
* $PostgreSQL: pgsql/src/bin/psql/copy.c,v 1.82 2009/08/07 20:16:11 tgl Exp $ * $PostgreSQL: pgsql/src/bin/psql/copy.c,v 1.83 2009/09/19 21:51:21 tgl Exp $
*/ */
#include "postgres_fe.h" #include "postgres_fe.h"
#include "copy.h" #include "copy.h"
@ -32,43 +32,28 @@
* -- parses \copy command line * -- parses \copy command line
* *
* The documented syntax is: * The documented syntax is:
* \copy tablename [(columnlist)] from|to filename * \copy tablename [(columnlist)] from|to filename [options]
* [ with ] [ binary ] [ oids ] [ delimiter [as] char ] [ null [as] string ] * \copy ( select stmt ) to filename [options]
* [ csv [ header ] [ quote [ AS ] string ] escape [as] string
* [ force not null column [, ...] | force quote column [, ...] | * ] ]
* *
* \copy ( select stmt ) to filename * An undocumented fact is that you can still write BINARY before the
* [ with ] [ binary ] [ delimiter [as] char ] [ null [as] string ] * tablename; this is a hangover from the pre-7.3 syntax. The options
* [ csv [ header ] [ quote [ AS ] string ] escape [as] string * syntax varies across backend versions, but we avoid all that mess
* [ force quote column [, ...] | * ] ] * by just transmitting the stuff after the filename literally.
*
* Force quote only applies for copy to; force not null only applies for
* copy from.
* *
* table name can be double-quoted and can have a schema part. * table name can be double-quoted and can have a schema part.
* column names can be double-quoted. * column names can be double-quoted.
* filename, char, and string can be single-quoted like SQL literals. * filename can be single-quoted like SQL literals.
* *
* returns a malloc'ed structure with the options, or NULL on parsing error * returns a malloc'ed structure with the options, or NULL on parsing error
*/ */
struct copy_options struct copy_options
{ {
char *table; char *before_tofrom; /* COPY string before TO/FROM */
char *column_list; char *after_tofrom; /* COPY string after TO/FROM filename */
char *file; /* NULL = stdin/stdout */ char *file; /* NULL = stdin/stdout */
bool psql_inout; /* true = use psql stdin/stdout */ bool psql_inout; /* true = use psql stdin/stdout */
bool from; bool from; /* true = FROM, false = TO */
bool binary;
bool oids;
bool csv_mode;
bool header;
char *delim;
char *null;
char *quote;
char *escape;
char *force_quote_list;
char *force_notnull_list;
}; };
@ -77,15 +62,9 @@ free_copy_options(struct copy_options * ptr)
{ {
if (!ptr) if (!ptr)
return; return;
free(ptr->table); free(ptr->before_tofrom);
free(ptr->column_list); free(ptr->after_tofrom);
free(ptr->file); free(ptr->file);
free(ptr->delim);
free(ptr->null);
free(ptr->quote);
free(ptr->escape);
free(ptr->force_quote_list);
free(ptr->force_notnull_list);
free(ptr); free(ptr);
} }
@ -108,14 +87,11 @@ static struct copy_options *
parse_slash_copy(const char *args) parse_slash_copy(const char *args)
{ {
struct copy_options *result; struct copy_options *result;
char *line;
char *token; char *token;
const char *whitespace = " \t\n\r"; const char *whitespace = " \t\n\r";
char nonstd_backslash = standard_strings() ? 0 : '\\'; char nonstd_backslash = standard_strings() ? 0 : '\\';
if (args) if (!args)
line = pg_strdup(args);
else
{ {
psql_error("\\copy: arguments required\n"); psql_error("\\copy: arguments required\n");
return NULL; return NULL;
@ -123,22 +99,23 @@ parse_slash_copy(const char *args)
result = pg_calloc(1, sizeof(struct copy_options)); result = pg_calloc(1, sizeof(struct copy_options));
token = strtokx(line, whitespace, ".,()", "\"", result->before_tofrom = pg_strdup(""); /* initialize for appending */
token = strtokx(args, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
goto error; goto error;
/* The following can be removed when we drop 7.3 syntax support */
if (pg_strcasecmp(token, "binary") == 0) if (pg_strcasecmp(token, "binary") == 0)
{ {
result->binary = true; xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, ".,()", "\"", token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
goto error; goto error;
} }
result->table = pg_strdup(token);
/* Handle COPY (SELECT) case */ /* Handle COPY (SELECT) case */
if (token[0] == '(') if (token[0] == '(')
{ {
@ -146,7 +123,9 @@ parse_slash_copy(const char *args)
while (parens > 0) while (parens > 0)
{ {
token = strtokx(NULL, whitespace, ".,()", "\"'", xstrcat(&result->before_tofrom, " ");
xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, "()", "\"'",
nonstd_backslash, true, false, pset.encoding); nonstd_backslash, true, false, pset.encoding);
if (!token) if (!token)
goto error; goto error;
@ -154,11 +133,11 @@ parse_slash_copy(const char *args)
parens++; parens++;
else if (token[0] == ')') else if (token[0] == ')')
parens--; parens--;
xstrcat(&result->table, " ");
xstrcat(&result->table, token);
} }
} }
xstrcat(&result->before_tofrom, " ");
xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, ".,()", "\"", token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
@ -171,12 +150,12 @@ parse_slash_copy(const char *args)
if (token[0] == '.') if (token[0] == '.')
{ {
/* handle schema . table */ /* handle schema . table */
xstrcat(&result->table, token); xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, ".,()", "\"", token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
goto error; goto error;
xstrcat(&result->table, token); xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, ".,()", "\"", token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
@ -186,24 +165,19 @@ parse_slash_copy(const char *args)
if (token[0] == '(') if (token[0] == '(')
{ {
/* handle parenthesized column list */ /* handle parenthesized column list */
result->column_list = pg_strdup(token);
for (;;) for (;;)
{ {
token = strtokx(NULL, whitespace, ".,()", "\"", xstrcat(&result->before_tofrom, " ");
0, false, false, pset.encoding); xstrcat(&result->before_tofrom, token);
if (!token || strchr(".,()", token[0])) token = strtokx(NULL, whitespace, "()", "\"",
goto error;
xstrcat(&result->column_list, token);
token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
goto error; goto error;
xstrcat(&result->column_list, token);
if (token[0] == ')') if (token[0] == ')')
break; break;
if (token[0] != ',')
goto error;
} }
xstrcat(&result->before_tofrom, " ");
xstrcat(&result->before_tofrom, token);
token = strtokx(NULL, whitespace, ".,()", "\"", token = strtokx(NULL, whitespace, ".,()", "\"",
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (!token) if (!token)
@ -241,156 +215,11 @@ parse_slash_copy(const char *args)
expand_tilde(&result->file); expand_tilde(&result->file);
} }
token = strtokx(NULL, whitespace, NULL, NULL, /* Collect the rest of the line (COPY options) */
token = strtokx(NULL, "", NULL, NULL,
0, false, false, pset.encoding); 0, false, false, pset.encoding);
if (token) if (token)
{ result->after_tofrom = pg_strdup(token);
/*
* WITH is optional. Also, the backend will allow WITH followed by
* nothing, so we do too.
*/
if (pg_strcasecmp(token, "with") == 0)
token = strtokx(NULL, whitespace, NULL, NULL,
0, false, false, pset.encoding);
while (token)
{
bool fetch_next;
fetch_next = true;
if (pg_strcasecmp(token, "oids") == 0)
result->oids = true;
else if (pg_strcasecmp(token, "binary") == 0)
result->binary = true;
else if (pg_strcasecmp(token, "csv") == 0)
result->csv_mode = true;
else if (pg_strcasecmp(token, "header") == 0)
result->header = true;
else if (pg_strcasecmp(token, "delimiter") == 0)
{
if (result->delim)
goto error;
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token && pg_strcasecmp(token, "as") == 0)
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token)
result->delim = pg_strdup(token);
else
goto error;
}
else if (pg_strcasecmp(token, "null") == 0)
{
if (result->null)
goto error;
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token && pg_strcasecmp(token, "as") == 0)
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token)
result->null = pg_strdup(token);
else
goto error;
}
else if (pg_strcasecmp(token, "quote") == 0)
{
if (result->quote)
goto error;
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token && pg_strcasecmp(token, "as") == 0)
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token)
result->quote = pg_strdup(token);
else
goto error;
}
else if (pg_strcasecmp(token, "escape") == 0)
{
if (result->escape)
goto error;
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token && pg_strcasecmp(token, "as") == 0)
token = strtokx(NULL, whitespace, NULL, "'",
nonstd_backslash, true, false, pset.encoding);
if (token)
result->escape = pg_strdup(token);
else
goto error;
}
else if (pg_strcasecmp(token, "force") == 0)
{
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (pg_strcasecmp(token, "quote") == 0)
{
if (result->force_quote_list)
goto error;
/* handle column list */
fetch_next = false;
for (;;)
{
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (!token || strchr(",", token[0]))
goto error;
if (!result->force_quote_list)
result->force_quote_list = pg_strdup(token);
else
xstrcat(&result->force_quote_list, token);
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (!token || token[0] != ',')
break;
xstrcat(&result->force_quote_list, token);
}
}
else if (pg_strcasecmp(token, "not") == 0)
{
if (result->force_notnull_list)
goto error;
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (pg_strcasecmp(token, "null") != 0)
goto error;
/* handle column list */
fetch_next = false;
for (;;)
{
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (!token || strchr(",", token[0]))
goto error;
if (!result->force_notnull_list)
result->force_notnull_list = pg_strdup(token);
else
xstrcat(&result->force_notnull_list, token);
token = strtokx(NULL, whitespace, ",", "\"",
0, false, false, pset.encoding);
if (!token || token[0] != ',')
break;
xstrcat(&result->force_notnull_list, token);
}
}
else
goto error;
}
else
goto error;
if (fetch_next)
token = strtokx(NULL, whitespace, NULL, NULL,
0, false, false, pset.encoding);
}
}
free(line);
return result; return result;
@ -400,29 +229,11 @@ error:
else else
psql_error("\\copy: parse error at end of line\n"); psql_error("\\copy: parse error at end of line\n");
free_copy_options(result); free_copy_options(result);
free(line);
return NULL; return NULL;
} }
/*
* Handle one of the "string" options of COPY. If the user gave a quoted
* string, pass it to the backend as-is; if it wasn't quoted then quote
* and escape it.
*/
static void
emit_copy_option(PQExpBuffer query, const char *keyword, const char *option)
{
appendPQExpBufferStr(query, keyword);
if (option[0] == '\'' ||
((option[0] == 'E' || option[0] == 'e') && option[1] == '\''))
appendPQExpBufferStr(query, option);
else
appendStringLiteralConn(query, option, pset.db);
}
/* /*
* Execute a \copy command (frontend copy). We have to open a file, then * 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 * submit a COPY query to the backend and either feed it data from the
@ -444,51 +255,7 @@ do_copy(const char *args)
if (!options) if (!options)
return false; return false;
initPQExpBuffer(&query); /* prepare to read or write the target file */
printfPQExpBuffer(&query, "COPY ");
appendPQExpBuffer(&query, "%s ", options->table);
if (options->column_list)
appendPQExpBuffer(&query, "%s ", options->column_list);
if (options->from)
appendPQExpBuffer(&query, "FROM STDIN");
else
appendPQExpBuffer(&query, "TO STDOUT");
if (options->binary)
appendPQExpBuffer(&query, " BINARY ");
if (options->oids)
appendPQExpBuffer(&query, " OIDS ");
if (options->delim)
emit_copy_option(&query, " DELIMITER ", options->delim);
if (options->null)
emit_copy_option(&query, " NULL AS ", options->null);
if (options->csv_mode)
appendPQExpBuffer(&query, " CSV");
if (options->header)
appendPQExpBuffer(&query, " HEADER");
if (options->quote)
emit_copy_option(&query, " QUOTE AS ", options->quote);
if (options->escape)
emit_copy_option(&query, " ESCAPE AS ", options->escape);
if (options->force_quote_list)
appendPQExpBuffer(&query, " FORCE QUOTE %s", options->force_quote_list);
if (options->force_notnull_list)
appendPQExpBuffer(&query, " FORCE NOT NULL %s", options->force_notnull_list);
if (options->file) if (options->file)
canonicalize_path(options->file); canonicalize_path(options->file);
@ -504,8 +271,7 @@ do_copy(const char *args)
else else
{ {
if (options->file) if (options->file)
copystream = fopen(options->file, copystream = fopen(options->file, PG_BINARY_W);
options->binary ? PG_BINARY_W : "w");
else if (!options->psql_inout) else if (!options->psql_inout)
copystream = pset.queryFout; copystream = pset.queryFout;
else else
@ -531,6 +297,17 @@ do_copy(const char *args)
return false; return false;
} }
/* build the command we will send to the backend */
initPQExpBuffer(&query);
printfPQExpBuffer(&query, "COPY ");
appendPQExpBufferStr(&query, options->before_tofrom);
if (options->from)
appendPQExpBuffer(&query, " FROM STDIN ");
else
appendPQExpBuffer(&query, " TO STDOUT ");
if (options->after_tofrom)
appendPQExpBufferStr(&query, options->after_tofrom);
result = PSQLexec(query.data, true); result = PSQLexec(query.data, true);
termPQExpBuffer(&query); termPQExpBuffer(&query);