Provide FORCE_NULL * and FORCE_NOT_NULL * options for COPY FROM
authorAndrew Dunstan <andrew@dunslane.net>
Sat, 30 Sep 2023 16:34:41 +0000 (12:34 -0400)
committerAndrew Dunstan <andrew@dunslane.net>
Sat, 30 Sep 2023 16:34:41 +0000 (12:34 -0400)
These options already exist, but you need to specify a column list for
them, which can be cumbersome. We already have the possibility of all
columns for FORCE QUOTE, so this is simply extending that facility to
FORCE_NULL and FORCE_NOT_NULL.

Author: Zhang Mingli
Reviewed-By: Richard Guo, Kyatoro Horiguchi, Michael Paquier.
Discussion: https://postgr.es/m/CACJufxEnVqzOFtqhexF2+AwOKFrV8zHOY3y=p+gPK6eB14pn_w@mail.gmail.com

doc/src/sgml/ref/copy.sgml
src/backend/commands/copy.c
src/backend/commands/copyfrom.c
src/backend/commands/copyto.c
src/backend/parser/gram.y
src/include/commands/copy.h
src/test/regress/expected/copy2.out
src/test/regress/sql/copy2.sql

index 4d614a0225de8f2db0bb873e8067faaf2bcb50ca..d12ba96497ae201527fbae2d17c1f1d8931eedc6 100644 (file)
@@ -41,8 +41,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
     QUOTE '<replaceable class="parameter">quote_character</replaceable>'
     ESCAPE '<replaceable class="parameter">escape_character</replaceable>'
     FORCE_QUOTE { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
-    FORCE_NOT_NULL ( <replaceable class="parameter">column_name</replaceable> [, ...] )
-    FORCE_NULL ( <replaceable class="parameter">column_name</replaceable> [, ...] )
+    FORCE_NOT_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
+    FORCE_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
     ENCODING '<replaceable class="parameter">encoding_name</replaceable>'
 </synopsis>
  </refsynopsisdiv>
@@ -350,6 +350,7 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       In the default case where the null string is empty, this means that
       empty values will be read as zero-length strings rather than nulls,
       even when they are not quoted.
+      If <literal>*</literal> is specified, the option will be applied to all columns.
       This option is allowed only in <command>COPY FROM</command>, and only when
       using <literal>CSV</literal> format.
      </para>
@@ -364,6 +365,7 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       if it has been quoted, and if a match is found set the value to
       <literal>NULL</literal>. In the default case where the null string is empty,
       this converts a quoted empty string into NULL.
+      If <literal>*</literal> is specified, the option will be applied to all columns.
       This option is allowed only in <command>COPY FROM</command>, and only when
       using <literal>CSV</literal> format.
      </para>
index f14fae330837c816ab9e4462e8f72caa2549b8f9..c5d7d78645adedc6768bee98527d5d3b982df4ec 100644 (file)
@@ -512,9 +512,11 @@ ProcessCopyOptions(ParseState *pstate,
        }
        else if (strcmp(defel->defname, "force_not_null") == 0)
        {
-           if (opts_out->force_notnull)
+           if (opts_out->force_notnull || opts_out->force_notnull_all)
                errorConflictingDefElem(defel, pstate);
-           if (defel->arg && IsA(defel->arg, List))
+           if (defel->arg && IsA(defel->arg, A_Star))
+               opts_out->force_notnull_all = true;
+           else if (defel->arg && IsA(defel->arg, List))
                opts_out->force_notnull = castNode(List, defel->arg);
            else
                ereport(ERROR,
@@ -525,9 +527,11 @@ ProcessCopyOptions(ParseState *pstate,
        }
        else if (strcmp(defel->defname, "force_null") == 0)
        {
-           if (opts_out->force_null)
+           if (opts_out->force_null || opts_out->force_null_all)
                errorConflictingDefElem(defel, pstate);
-           if (defel->arg && IsA(defel->arg, List))
+           if (defel->arg && IsA(defel->arg, A_Star))
+               opts_out->force_null_all = true;
+           else if (defel->arg && IsA(defel->arg, List))
                opts_out->force_null = castNode(List, defel->arg);
            else
                ereport(ERROR,
index 70871ed819013a3640b0cb3e430595b47ffa6782..2d1567d0e42ae5f6ec150964605d4c7f9ad3b3aa 100644 (file)
@@ -1393,7 +1393,9 @@ BeginCopyFrom(ParseState *pstate,
 
    /* Convert FORCE_NOT_NULL name list to per-column flags, check validity */
    cstate->opts.force_notnull_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool));
-   if (cstate->opts.force_notnull)
+   if (cstate->opts.force_notnull_all)
+       MemSet(cstate->opts.force_notnull_flags, true, num_phys_attrs * sizeof(bool));
+   else if (cstate->opts.force_notnull)
    {
        List       *attnums;
        ListCell   *cur;
@@ -1416,7 +1418,9 @@ BeginCopyFrom(ParseState *pstate,
 
    /* Convert FORCE_NULL name list to per-column flags, check validity */
    cstate->opts.force_null_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool));
-   if (cstate->opts.force_null)
+   if (cstate->opts.force_null_all)
+       MemSet(cstate->opts.force_null_flags, true, num_phys_attrs * sizeof(bool));
+   else if (cstate->opts.force_null)
    {
        List       *attnums;
        ListCell   *cur;
index eaa3172793a48813e165a539cff5c8633faaf4ac..0378f0ade090cd59256ee328cfe302d9afe5e694 100644 (file)
@@ -582,10 +582,7 @@ BeginCopyTo(ParseState *pstate,
    cstate->opts.force_quote_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool));
    if (cstate->opts.force_quote_all)
    {
-       int         i;
-
-       for (i = 0; i < num_phys_attrs; i++)
-           cstate->opts.force_quote_flags[i] = true;
+       MemSet(cstate->opts.force_quote_flags, true, num_phys_attrs * sizeof(bool));
    }
    else if (cstate->opts.force_quote)
    {
index 7d2032885edfd67b01277a439698e61ab19600dd..fec9b2604febc4506f6755a3159b9d6d4ad37279 100644 (file)
@@ -3419,10 +3419,18 @@ copy_opt_item:
                {
                    $$ = makeDefElem("force_not_null", (Node *) $4, @1);
                }
+           | FORCE NOT NULL_P '*'
+               {
+                   $$ = makeDefElem("force_not_null", (Node *) makeNode(A_Star), @1);
+               }
            | FORCE NULL_P columnList
                {
                    $$ = makeDefElem("force_null", (Node *) $3, @1);
                }
+           | FORCE NULL_P '*'
+               {
+                   $$ = makeDefElem("force_null", (Node *) makeNode(A_Star), @1);
+               }
            | ENCODING Sconst
                {
                    $$ = makeDefElem("encoding", (Node *) makeString($2), @1);
index 33175868f653af4b9e1156106d5dbd4ada919e4d..f2cca0b90b494dba8429f17b0f883d6a1a7951e6 100644 (file)
@@ -56,8 +56,10 @@ typedef struct CopyFormatOptions
    bool        force_quote_all;    /* FORCE_QUOTE *? */
    bool       *force_quote_flags;  /* per-column CSV FQ flags */
    List       *force_notnull;  /* list of column names */
+   bool        force_notnull_all;  /* FORCE_NOT_NULL *? */
    bool       *force_notnull_flags;    /* per-column CSV FNN flags */
    List       *force_null;     /* list of column names */
+   bool        force_null_all; /* FORCE_NULL *? */
    bool       *force_null_flags;   /* per-column CSV FN flags */
    bool        convert_selectively;    /* do selective binary conversion? */
    List       *convert_select; /* list of column names (can be NIL) */
index faf1a4d1b0c6044340db3b5eaf20f98c22eab519..95ec7363afcc5cde55c21d45e59b3eccefa63231 100644 (file)
@@ -520,6 +520,50 @@ BEGIN;
 COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b));
 ERROR:  FORCE_NULL column "b" not referenced by COPY
 ROLLBACK;
+-- should succeed with no effect ("b" remains an empty string, "c" remains NULL)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *, FORCE_NULL *);
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 4;
+ b |  c   
+---+------
+   | NULL
+(1 row)
+
+-- should succeed with effect ("b" remains an empty string)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *);
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 5;
+ b | c 
+---+---
+   | 
+(1 row)
+
+-- should succeed with effect ("c" remains NULL)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL *);
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 6;
+ b |  c   
+---+------
+ b | NULL
+(1 row)
+
+-- should fail with "conflicting or redundant options" error
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *, FORCE_NOT_NULL(b));
+ERROR:  conflicting or redundant options
+LINE 1: ...c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *, FORCE_NOT_...
+                                                             ^
+ROLLBACK;
+-- should fail with "conflicting or redundant options" error
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL *, FORCE_NULL(b));
+ERROR:  conflicting or redundant options
+LINE 1: ... b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL *, FORCE_NULL...
+                                                             ^
+ROLLBACK;
 \pset null ''
 -- test case with whole-row Var in a check constraint
 create table check_con_tbl (f1 int);
index d759635068c1c5d7afa8bc8a85b19f9f0a996732..a5486f60867bfbea99a3a630c31352b49f674b29 100644 (file)
@@ -344,6 +344,36 @@ ROLLBACK;
 BEGIN;
 COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b));
 ROLLBACK;
+-- should succeed with no effect ("b" remains an empty string, "c" remains NULL)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *, FORCE_NULL *);
+4,,""
+\.
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 4;
+-- should succeed with effect ("b" remains an empty string)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *);
+5,,""
+\.
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 5;
+-- should succeed with effect ("c" remains NULL)
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL *);
+6,"b",""
+\.
+COMMIT;
+SELECT b, c FROM forcetest WHERE a = 6;
+-- should fail with "conflicting or redundant options" error
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *, FORCE_NOT_NULL(b));
+ROLLBACK;
+-- should fail with "conflicting or redundant options" error
+BEGIN;
+COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL *, FORCE_NULL(b));
+ROLLBACK;
+
 \pset null ''
 
 -- test case with whole-row Var in a check constraint