Provide FORCE_NULL * and FORCE_NOT_NULL * options for COPY FROM

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
This commit is contained in:
Andrew Dunstan 2023-09-30 12:34:41 -04:00
parent c181f2e2bc
commit f6d4c9cf16
8 changed files with 103 additions and 12 deletions

View File

@ -41,8 +41,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
QUOTE '<replaceable class="parameter">quote_character</replaceable>' QUOTE '<replaceable class="parameter">quote_character</replaceable>'
ESCAPE '<replaceable class="parameter">escape_character</replaceable>' ESCAPE '<replaceable class="parameter">escape_character</replaceable>'
FORCE_QUOTE { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * } FORCE_QUOTE { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
FORCE_NOT_NULL ( <replaceable class="parameter">column_name</replaceable> [, ...] ) FORCE_NOT_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
FORCE_NULL ( <replaceable class="parameter">column_name</replaceable> [, ...] ) FORCE_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
ENCODING '<replaceable class="parameter">encoding_name</replaceable>' ENCODING '<replaceable class="parameter">encoding_name</replaceable>'
</synopsis> </synopsis>
</refsynopsisdiv> </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 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, empty values will be read as zero-length strings rather than nulls,
even when they are not quoted. 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 This option is allowed only in <command>COPY FROM</command>, and only when
using <literal>CSV</literal> format. using <literal>CSV</literal> format.
</para> </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 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, <literal>NULL</literal>. In the default case where the null string is empty,
this converts a quoted empty string into NULL. 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 This option is allowed only in <command>COPY FROM</command>, and only when
using <literal>CSV</literal> format. using <literal>CSV</literal> format.
</para> </para>

View File

@ -512,9 +512,11 @@ ProcessCopyOptions(ParseState *pstate,
} }
else if (strcmp(defel->defname, "force_not_null") == 0) 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); 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); opts_out->force_notnull = castNode(List, defel->arg);
else else
ereport(ERROR, ereport(ERROR,
@ -525,9 +527,11 @@ ProcessCopyOptions(ParseState *pstate,
} }
else if (strcmp(defel->defname, "force_null") == 0) 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); 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); opts_out->force_null = castNode(List, defel->arg);
else else
ereport(ERROR, ereport(ERROR,

View File

@ -1393,7 +1393,9 @@ BeginCopyFrom(ParseState *pstate,
/* Convert FORCE_NOT_NULL name list to per-column flags, check validity */ /* Convert FORCE_NOT_NULL name list to per-column flags, check validity */
cstate->opts.force_notnull_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); 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; List *attnums;
ListCell *cur; ListCell *cur;
@ -1416,7 +1418,9 @@ BeginCopyFrom(ParseState *pstate,
/* Convert FORCE_NULL name list to per-column flags, check validity */ /* Convert FORCE_NULL name list to per-column flags, check validity */
cstate->opts.force_null_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); 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; List *attnums;
ListCell *cur; ListCell *cur;

View File

@ -582,10 +582,7 @@ BeginCopyTo(ParseState *pstate,
cstate->opts.force_quote_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); cstate->opts.force_quote_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool));
if (cstate->opts.force_quote_all) if (cstate->opts.force_quote_all)
{ {
int i; MemSet(cstate->opts.force_quote_flags, true, num_phys_attrs * sizeof(bool));
for (i = 0; i < num_phys_attrs; i++)
cstate->opts.force_quote_flags[i] = true;
} }
else if (cstate->opts.force_quote) else if (cstate->opts.force_quote)
{ {

View File

@ -3419,10 +3419,18 @@ copy_opt_item:
{ {
$$ = makeDefElem("force_not_null", (Node *) $4, @1); $$ = makeDefElem("force_not_null", (Node *) $4, @1);
} }
| FORCE NOT NULL_P '*'
{
$$ = makeDefElem("force_not_null", (Node *) makeNode(A_Star), @1);
}
| FORCE NULL_P columnList | FORCE NULL_P columnList
{ {
$$ = makeDefElem("force_null", (Node *) $3, @1); $$ = makeDefElem("force_null", (Node *) $3, @1);
} }
| FORCE NULL_P '*'
{
$$ = makeDefElem("force_null", (Node *) makeNode(A_Star), @1);
}
| ENCODING Sconst | ENCODING Sconst
{ {
$$ = makeDefElem("encoding", (Node *) makeString($2), @1); $$ = makeDefElem("encoding", (Node *) makeString($2), @1);

View File

@ -56,8 +56,10 @@ typedef struct CopyFormatOptions
bool force_quote_all; /* FORCE_QUOTE *? */ bool force_quote_all; /* FORCE_QUOTE *? */
bool *force_quote_flags; /* per-column CSV FQ flags */ bool *force_quote_flags; /* per-column CSV FQ flags */
List *force_notnull; /* list of column names */ List *force_notnull; /* list of column names */
bool force_notnull_all; /* FORCE_NOT_NULL *? */
bool *force_notnull_flags; /* per-column CSV FNN flags */ bool *force_notnull_flags; /* per-column CSV FNN flags */
List *force_null; /* list of column names */ List *force_null; /* list of column names */
bool force_null_all; /* FORCE_NULL *? */
bool *force_null_flags; /* per-column CSV FN flags */ bool *force_null_flags; /* per-column CSV FN flags */
bool convert_selectively; /* do selective binary conversion? */ bool convert_selectively; /* do selective binary conversion? */
List *convert_select; /* list of column names (can be NIL) */ List *convert_select; /* list of column names (can be NIL) */

View File

@ -520,6 +520,50 @@ BEGIN;
COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b)); COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b));
ERROR: FORCE_NULL column "b" not referenced by COPY ERROR: FORCE_NULL column "b" not referenced by COPY
ROLLBACK; 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 '' \pset null ''
-- test case with whole-row Var in a check constraint -- test case with whole-row Var in a check constraint
create table check_con_tbl (f1 int); create table check_con_tbl (f1 int);

View File

@ -344,6 +344,36 @@ ROLLBACK;
BEGIN; BEGIN;
COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b)); COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b));
ROLLBACK; 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 '' \pset null ''
-- test case with whole-row Var in a check constraint -- test case with whole-row Var in a check constraint