Remove a couple hundred lines of ugly and tedious-to-maintain code by not
authorTom Lane
Sat, 19 Sep 2009 21:51:21 +0000 (21:51 +0000)
committerTom Lane
Sat, 19 Sep 2009 21:51:21 +0000 (21:51 +0000)
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

src/bin/psql/copy.c

index 5b91df26b407d9a0cca27cf00fe40ca2085d16c1..ae389cfdae08927d48d4b72c75628cb71307e15e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * 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 "copy.h"
  * -- parses \copy command line
  *
  * The documented syntax is:
- * \copy tablename [(columnlist)] from|to filename
- *   [ with ] [ binary ] [ oids ] [ delimiter [as] char ] [ null [as] string ]
- *   [ csv  [ header ] [ quote [ AS ] string ]  escape [as] string
- *     [ force not null column [, ...] | force quote column [, ...] | * ] ]
+ * \copy tablename [(columnlist)] from|to filename [options]
+ * \copy ( select stmt ) to filename [options]
  *
- * \copy ( select stmt ) to filename
- *   [ with ] [ binary ] [ delimiter [as] char ] [ null [as] string ]
- *   [ csv  [ header ] [ quote [ AS ] string ]  escape [as] string
- *     [ force quote column [, ...] | * ] ]
- *
- * Force quote only applies for copy to; force not null only applies for
- * copy from.
+ * 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
+ * syntax varies across backend versions, but we avoid all that mess
+ * by just transmitting the stuff after the filename literally.
  *
  * table name can be double-quoted and can have a schema part.
  * 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
  */
 
 struct copy_options
 {
-   char       *table;
-   char       *column_list;
+   char       *before_tofrom;  /* COPY string before TO/FROM */
+   char       *after_tofrom;   /* COPY string after TO/FROM filename */
    char       *file;           /* NULL = stdin/stdout */
    bool        psql_inout;     /* true = use psql stdin/stdout */
-   bool        from;
-   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;
+   bool        from;           /* true = FROM, false = TO */
 };
 
 
@@ -77,15 +62,9 @@ free_copy_options(struct copy_options * ptr)
 {
    if (!ptr)
        return;
-   free(ptr->table);
-   free(ptr->column_list);
+   free(ptr->before_tofrom);
+   free(ptr->after_tofrom);
    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);
 }
 
@@ -108,14 +87,11 @@ static struct copy_options *
 parse_slash_copy(const char *args)
 {
    struct copy_options *result;
-   char       *line;
    char       *token;
    const char *whitespace = " \t\n\r";
    char        nonstd_backslash = standard_strings() ? 0 : '\\';
 
-   if (args)
-       line = pg_strdup(args);
-   else
+   if (!args)
    {
        psql_error("\\copy: arguments required\n");
        return NULL;
@@ -123,22 +99,23 @@ parse_slash_copy(const char *args)
 
    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);
    if (!token)
        goto error;
 
+   /* The following can be removed when we drop 7.3 syntax support */
    if (pg_strcasecmp(token, "binary") == 0)
    {
-       result->binary = true;
+       xstrcat(&result->before_tofrom, token);
        token = strtokx(NULL, whitespace, ".,()", "\"",
                        0, false, false, pset.encoding);
        if (!token)
            goto error;
    }
 
-   result->table = pg_strdup(token);
-
    /* Handle COPY (SELECT) case */
    if (token[0] == '(')
    {
@@ -146,7 +123,9 @@ parse_slash_copy(const char *args)
 
        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);
            if (!token)
                goto error;
@@ -154,11 +133,11 @@ parse_slash_copy(const char *args)
                parens++;
            else if (token[0] == ')')
                parens--;
-           xstrcat(&result->table, " ");
-           xstrcat(&result->table, token);
        }
    }
 
+   xstrcat(&result->before_tofrom, " ");
+   xstrcat(&result->before_tofrom, token);
    token = strtokx(NULL, whitespace, ".,()", "\"",
                    0, false, false, pset.encoding);
    if (!token)
@@ -171,12 +150,12 @@ parse_slash_copy(const char *args)
    if (token[0] == '.')
    {
        /* handle schema . table */
-       xstrcat(&result->table, token);
+       xstrcat(&result->before_tofrom, token);
        token = strtokx(NULL, whitespace, ".,()", "\"",
                        0, false, false, pset.encoding);
        if (!token)
            goto error;
-       xstrcat(&result->table, token);
+       xstrcat(&result->before_tofrom, token);
        token = strtokx(NULL, whitespace, ".,()", "\"",
                        0, false, false, pset.encoding);
        if (!token)
@@ -186,24 +165,19 @@ parse_slash_copy(const char *args)
    if (token[0] == '(')
    {
        /* handle parenthesized column list */
-       result->column_list = pg_strdup(token);
        for (;;)
        {
-           token = strtokx(NULL, whitespace, ".,()", "\"",
-                           0, false, false, pset.encoding);
-           if (!token || strchr(".,()", token[0]))
-               goto error;
-           xstrcat(&result->column_list, token);
-           token = strtokx(NULL, whitespace, ".,()", "\"",
+           xstrcat(&result->before_tofrom, " ");
+           xstrcat(&result->before_tofrom, token);
+           token = strtokx(NULL, whitespace, "()", "\"",
                            0, false, false, pset.encoding);
            if (!token)
                goto error;
-           xstrcat(&result->column_list, token);
            if (token[0] == ')')
                break;
-           if (token[0] != ',')
-               goto error;
        }
+       xstrcat(&result->before_tofrom, " ");
+       xstrcat(&result->before_tofrom, token);
        token = strtokx(NULL, whitespace, ".,()", "\"",
                        0, false, false, pset.encoding);
        if (!token)
@@ -241,156 +215,11 @@ parse_slash_copy(const char *args)
        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);
-
    if (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);
+       result->after_tofrom = pg_strdup(token);
 
    return result;
 
@@ -400,29 +229,11 @@ error:
    else
        psql_error("\\copy: parse error at end of line\n");
    free_copy_options(result);
-   free(line);
 
    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
  * 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)
        return false;
 
-   initPQExpBuffer(&query);
-
-   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);
-
+   /* prepare to read or write the target file */
    if (options->file)
        canonicalize_path(options->file);
 
@@ -504,8 +271,7 @@ do_copy(const char *args)
    else
    {
        if (options->file)
-           copystream = fopen(options->file,
-                              options->binary ? PG_BINARY_W : "w");
+           copystream = fopen(options->file, PG_BINARY_W);
        else if (!options->psql_inout)
            copystream = pset.queryFout;
        else
@@ -531,6 +297,17 @@ do_copy(const char *args)
        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);
    termPQExpBuffer(&query);