diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml new file mode 100644 index 1ecc939..0038d03 *** a/doc/src/sgml/ref/copy.sgml --- b/doc/src/sgml/ref/copy.sgml *************** COPY { ta *** 42,47 **** --- 42,48 ---- ESCAPE 'escape_character' FORCE_QUOTE { ( column_name [, ...] ) | * } FORCE_NOT_NULL ( column_name [, ...] ) + FORCE_NULL ( column_name [, ...] ) ENCODING 'encoding_name' *************** COPY { ta *** 329,334 **** --- 330,347 ---- + FORCE_NULL + + + Force the specified columns' values to be converted to NULL + if the value contains an empty string. + This option is allowed only in COPY FROM, and only when + using CSV format. + + + + + ENCODING *************** COPY coun *** 637,643 **** string, while an empty string data value is written with double quotes (""). Reading values follows similar rules. You can use FORCE_NOT_NULL to prevent NULL input ! comparisons for specific columns. --- 650,658 ---- string, while an empty string data value is written with double quotes (""). Reading values follows similar rules. You can use FORCE_NOT_NULL to prevent NULL input ! comparisons for specific columns. Alternatively you can use ! FORCE_NULL to convert empty string data values to ! NULL. diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c new file mode 100644 index 6b20144..40a37a8 *** a/src/backend/commands/copy.c --- b/src/backend/commands/copy.c *************** typedef struct CopyStateData *** 125,130 **** --- 125,132 ---- bool *force_quote_flags; /* per-column CSV FQ flags */ List *force_notnull; /* list of column names */ bool *force_notnull_flags; /* per-column CSV FNN flags */ + List *force_null; /* list of column names */ + 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) */ bool *convert_select_flags; /* per-column CSV/TEXT CS flags */ *************** ProcessCopyOptions(CopyState cstate, *** 1019,1024 **** --- 1021,1040 ---- errmsg("argument to option \"%s\" must be a list of column names", defel->defname))); } + else if (strcmp(defel->defname, "force_null") == 0) + { + if (cstate->force_null) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + if (defel->arg && IsA(defel->arg, List)) + cstate->force_null = (List *) defel->arg; + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument to option \"%s\" must be a list of column names", + defel->defname))); + } else if (strcmp(defel->defname, "convert_selectively") == 0) { /* *************** ProcessCopyOptions(CopyState cstate, *** 1178,1183 **** --- 1194,1210 ---- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("COPY force not null only available using COPY FROM"))); + /* Check force_null */ + if (!cstate->csv_mode && cstate->force_null != NIL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY force null available only in CSV mode"))); + + if (cstate->force_notnull != NIL && !is_from) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY force null only available using COPY FROM"))); + /* Don't allow the delimiter to appear in the null string. */ if (strchr(cstate->null_print, cstate->delim[0]) != NULL) ereport(ERROR, *************** BeginCopy(bool is_from, *** 1385,1390 **** --- 1412,1439 ---- } } + /* Convert FORCE NULL name list to per-column flags, check validity */ + cstate->force_null_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); + if (cstate->force_null) + { + List *attnums; + ListCell *cur; + + attnums = CopyGetAttnums(tupDesc, cstate->rel, cstate->force_null); + + foreach(cur, attnums) + { + int attnum = lfirst_int(cur); + + if (!list_member_int(cstate->attnumlist, attnum)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("FORCE NULL column \"%s\" not referenced by COPY", + NameStr(tupDesc->attrs[attnum - 1]->attname)))); + cstate->force_null_flags[attnum - 1] = true; + } + } + /* Convert convert_selectively name list to per-column flags */ if (cstate->convert_selectively) { *************** NextCopyFrom(CopyState cstate, ExprConte *** 2795,2805 **** continue; } ! if (cstate->csv_mode && string == NULL && ! cstate->force_notnull_flags[m]) { ! /* Go ahead and read the NULL string */ ! string = cstate->null_print; } cstate->cur_attname = NameStr(attr[m]->attname); --- 2844,2867 ---- continue; } ! if (cstate->csv_mode) { ! if(string == NULL && ! cstate->force_notnull_flags[m]) ! { ! /* FORCE_NOT_NULL option is set and column is NULL - ! convert it to an empty string ! */ ! string = cstate->null_print; ! } ! else if(string != NULL && strlen(string) == 0 && ! cstate->force_null_flags[m]) ! { ! /* FORCE_NULL option is set and column is an empty string - ! convert it to NULL ! */ ! string = NULL; ! } } cstate->cur_attname = NameStr(attr[m]->attname); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index a9812af..ca57ab7 *** a/src/backend/parser/gram.y --- b/src/backend/parser/gram.y *************** copy_opt_item: *** 2493,2498 **** --- 2493,2502 ---- { $$ = makeDefElem("force_not_null", (Node *)$4); } + | FORCE NULL_P columnList + { + $$ = makeDefElem("force_null", (Node *)$3); + } | ENCODING Sconst { $$ = makeDefElem("encoding", (Node *)makeString($2)); diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out new file mode 100644 index 34fa131..de99108 *** a/src/test/regress/expected/copy2.out --- b/src/test/regress/expected/copy2.out *************** SELECT * FROM vistest; *** 382,387 **** --- 382,435 ---- e (2 rows) + -- Test FORCE_NOT_NULL and FORCE_NULL options + -- should succeed with "b" set to an empty string and "c" set to NULL + CREATE TEMP TABLE forcetest ( + a INT NOT NULL, + b TEXT NOT NULL, + c TEXT, + d TEXT, + e TEXT + ); + \pset null NULL + BEGIN; + COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL(b), FORCE_NULL(c)); + COMMIT; + SELECT b, c FROM forcetest WHERE a = 1; + b | c + ---+------ + | NULL + (1 row) + + -- 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(b), FORCE_NULL(c)); + COMMIT; + SELECT b, c FROM forcetest WHERE a = 2; + b | c + ---+------ + | NULL + (1 row) + + -- should fail with not-null constraint violiaton + BEGIN; + COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b), FORCE_NOT_NULL(c)); + ERROR: null value in column "b" violates not-null constraint + DETAIL: Failing row contains (3, null, , null, null). + CONTEXT: COPY forcetest, line 1: "3,,""" + ROLLBACK; + -- should fail with "not referenced by COPY" error + BEGIN; + COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL(b)); + ERROR: FORCE NOT NULL column "b" not referenced by COPY + ROLLBACK; + -- should fail with "not referenced by COPY" error + BEGIN; + COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b)); + ERROR: FORCE NULL column "b" not referenced by COPY + ROLLBACK; + \pset null '' + DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact(); DROP TABLE x, y; diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql new file mode 100644 index c46128b..b417cf7 *** a/src/test/regress/sql/copy2.sql --- b/src/test/regress/sql/copy2.sql *************** e *** 270,275 **** --- 270,314 ---- SELECT * FROM vistest; COMMIT; SELECT * FROM vistest; + -- Test FORCE_NOT_NULL and FORCE_NULL options + -- should succeed with "b" set to an empty string and "c" set to NULL + CREATE TEMP TABLE forcetest ( + a INT NOT NULL, + b TEXT NOT NULL, + c TEXT, + d TEXT, + e TEXT + ); + \pset null NULL + BEGIN; + COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL(b), FORCE_NULL(c)); + 1,,"" + \. + COMMIT; + SELECT b, c FROM forcetest WHERE a = 1; + -- 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(b), FORCE_NULL(c)); + 2,,"" + \. + COMMIT; + SELECT b, c FROM forcetest WHERE a = 2; + -- should fail with not-null constraint violiaton + BEGIN; + COPY forcetest (a, b, c) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b), FORCE_NOT_NULL(c)); + 3,,"" + \. + ROLLBACK; + -- should fail with "not referenced by COPY" error + BEGIN; + COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL(b)); + ROLLBACK; + -- should fail with "not referenced by COPY" error + BEGIN; + COPY forcetest (d, e) FROM STDIN WITH (FORMAT csv, FORCE_NULL(b)); + ROLLBACK; + \pset null '' + DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact(); DROP TABLE x, y;