diff --git a/contrib/file_fdw/data/copy_default.csv b/contrib/file_fdw/data/copy_default.csv
new file mode 100644
index 0000000000..5e83a15db4
--- /dev/null
+++ b/contrib/file_fdw/data/copy_default.csv
@@ -0,0 +1,3 @@
+1,value,2022-07-04
+2,\D,2022-07-03
+3,\D,\D
diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 36d76ba26c..f5ae29732a 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -424,6 +424,23 @@ SELECT a, c FROM gft1;
(2 rows)
DROP FOREIGN TABLE gft1;
+-- copy default tests
+\set filename :abs_srcdir '/data/copy_default.csv'
+CREATE FOREIGN TABLE copy_default (
+ id integer,
+ text_value text not null default 'test',
+ ts_value timestamp without time zone not null default '2022-07-05'
+) SERVER file_server
+OPTIONS (format 'csv', filename :'filename', default '\D');
+SELECT id, text_value, ts_value FROM copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | value | Mon Jul 04 00:00:00 2022
+ 2 | test | Sun Jul 03 00:00:00 2022
+ 3 | test | Tue Jul 05 00:00:00 2022
+(3 rows)
+
+DROP FOREIGN TABLE copy_default;
-- privilege tests
SET ROLE regress_file_fdw_superuser;
SELECT * FROM agg_text ORDER BY a;
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 2d2b0b6a6b..99b21e8316 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -72,6 +72,7 @@ static const struct FileFdwOption valid_options[] = {
{"quote", ForeignTableRelationId},
{"escape", ForeignTableRelationId},
{"null", ForeignTableRelationId},
+ {"default", ForeignTableRelationId},
{"encoding", ForeignTableRelationId},
{"force_not_null", AttributeRelationId},
{"force_null", AttributeRelationId},
@@ -712,6 +713,9 @@ static TupleTableSlot *
fileIterateForeignScan(ForeignScanState *node)
{
FileFdwExecutionState *festate = (FileFdwExecutionState *) node->fdw_state;
+ EState *estate = CreateExecutorState();
+ ExprContext *econtext;
+ MemoryContext oldcontext;
TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
bool found;
ErrorContextCallback errcallback;
@@ -728,15 +732,25 @@ fileIterateForeignScan(ForeignScanState *node)
* ExecStoreVirtualTuple. If we don't find another row in the file, we
* just skip the last step, leaving the slot empty as required.
*
- * We can pass ExprContext = NULL because we read all columns from the
- * file, so no need to evaluate default expressions.
+ * We pass ExprContext because there might be a use of the DEFAULT option
+ * in COPY FROM, so we may need to evaluate default expressions.
*/
ExecClearTuple(slot);
- found = NextCopyFrom(festate->cstate, NULL,
+ econtext = GetPerTupleExprContext(estate);
+
+ /*
+ * DEFAULT expressions need to be evaluated in a per-tuple context, so
+ * switch in case we are doing that.
+ */
+ oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+ found = NextCopyFrom(festate->cstate, econtext,
slot->tts_values, slot->tts_isnull);
if (found)
ExecStoreVirtualTuple(slot);
+ /* Switch back to original memory context */
+ MemoryContextSwitchTo(oldcontext);
+
/* Remove error callback. */
error_context_stack = errcallback.previous;
diff --git a/contrib/file_fdw/sql/file_fdw.sql b/contrib/file_fdw/sql/file_fdw.sql
index 46670397ca..f0548e14e1 100644
--- a/contrib/file_fdw/sql/file_fdw.sql
+++ b/contrib/file_fdw/sql/file_fdw.sql
@@ -233,6 +233,17 @@ OPTIONS (format 'csv', filename :'filename', delimiter ',');
SELECT a, c FROM gft1;
DROP FOREIGN TABLE gft1;
+-- copy default tests
+\set filename :abs_srcdir '/data/copy_default.csv'
+CREATE FOREIGN TABLE copy_default (
+ id integer,
+ text_value text not null default 'test',
+ ts_value timestamp without time zone not null default '2022-07-05'
+) SERVER file_server
+OPTIONS (format 'csv', filename :'filename', default '\D');
+SELECT id, text_value, ts_value FROM copy_default;
+DROP FOREIGN TABLE copy_default;
+
-- privilege tests
SET ROLE regress_file_fdw_superuser;
SELECT * FROM agg_text ORDER BY a;
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..5e591ed2e6 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -43,6 +43,7 @@ COPY { table_name [ ( column_name [, ...] )
FORCE_NULL ( column_name [, ...] )
ENCODING 'encoding_name'
+ DEFAULT 'default_string'
@@ -368,6 +369,19 @@ COPY { table_name [ (
+
+ DEFAULT
+
+
+ Specifies the string that represents a default value. Each time the string
+ is found in the input file, the default value of the corresponding column
+ will be used.
+ This option is allowed only in COPY FROM, and only when
+ not using binary format.
+
+
+
+
WHERE
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index f3bbd91fe3..167d31a2d9 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -464,6 +464,12 @@ ProcessCopyOptions(ParseState *pstate,
errorConflictingDefElem(defel, pstate);
opts_out->null_print = defGetString(defel);
}
+ else if (strcmp(defel->defname, "default") == 0)
+ {
+ if (opts_out->default_print)
+ errorConflictingDefElem(defel, pstate);
+ opts_out->default_print = defGetString(defel);
+ }
else if (strcmp(defel->defname, "header") == 0)
{
if (header_specified)
@@ -577,6 +583,11 @@ ProcessCopyOptions(ParseState *pstate,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot specify NULL in BINARY mode")));
+ if (opts_out->binary && opts_out->default_print)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot specify DEFAULT in BINARY mode")));
+
/* Set defaults for omitted options */
if (!opts_out->delim)
opts_out->delim = opts_out->csv_mode ? "," : "\t";
@@ -612,6 +623,17 @@ ProcessCopyOptions(ParseState *pstate,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("COPY null representation cannot use newline or carriage return")));
+ if (opts_out->default_print)
+ {
+ opts_out->default_print_len = strlen(opts_out->default_print);
+
+ if (strchr(opts_out->default_print, '\r') != NULL ||
+ strchr(opts_out->default_print, '\n') != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("COPY default representation cannot use newline or carriage return")));
+ }
+
/*
* Disallow unsafe delimiter characters in non-CSV mode. We can't allow
* backslash because it would be ambiguous. We can't allow the other
@@ -705,6 +727,35 @@ ProcessCopyOptions(ParseState *pstate,
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("CSV quote character must not appear in the NULL specification")));
+
+ if (opts_out->default_print)
+ {
+ if (!is_from)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("COPY DEFAULT only available using COPY FROM")));
+
+ /* Don't allow the delimiter to appear in the default string. */
+ if (strchr(opts_out->default_print, opts_out->delim[0]) != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("COPY delimiter must not appear in the DEFAULT specification")));
+
+ /* Don't allow the CSV quote char to appear in the default string. */
+ if (opts_out->csv_mode &&
+ strchr(opts_out->default_print, opts_out->quote[0]) != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("CSV quote character must not appear in the DEFAULT specification")));
+
+ /* Don't allow the NULL and DEFAULT string to be the same */
+ if (opts_out->null_print_len == opts_out->default_print_len &&
+ strncmp(opts_out->null_print, opts_out->default_print,
+ opts_out->null_print_len) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("NULL specification and DEFAULT specification cannot be the same")));
+ }
}
/*
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 29cd1cf4a6..321a7fad85 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1565,11 +1565,11 @@ BeginCopyFrom(ParseState *pstate,
&in_func_oid, &typioparams[attnum - 1]);
fmgr_info(in_func_oid, &in_functions[attnum - 1]);
- /* Get default info if needed */
- if (!list_member_int(cstate->attnumlist, attnum) && !att->attgenerated)
+ /* Get default info if available */
+ defexprs[attnum - 1] = NULL;
+
+ if (!att->attgenerated)
{
- /* attribute is NOT to be copied from input */
- /* use default value if one exists */
Expr *defexpr = (Expr *) build_column_default(cstate->rel,
attnum);
@@ -1579,9 +1579,15 @@ BeginCopyFrom(ParseState *pstate,
defexpr = expression_planner(defexpr);
/* Initialize executable expression in copycontext */
- defexprs[num_defaults] = ExecInitExpr(defexpr, NULL);
- defmap[num_defaults] = attnum - 1;
- num_defaults++;
+ defexprs[attnum - 1] = ExecInitExpr(defexpr, NULL);
+
+ /* if NOT copied from input */
+ /* use default value if one exists */
+ if (!list_member_int(cstate->attnumlist, attnum))
+ {
+ defmap[num_defaults] = attnum - 1;
+ num_defaults++;
+ }
/*
* If a default expression looks at the table being loaded,
diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index 91b564c2bc..c346486cd3 100644
--- a/src/backend/commands/copyfromparse.c
+++ b/src/backend/commands/copyfromparse.c
@@ -842,9 +842,10 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields)
/*
* Read next tuple from file for COPY FROM. Return false if no more tuples.
*
- * 'econtext' is used to evaluate default expression for each column not
- * read from the file. It can be NULL when no default values are used, i.e.
- * when all columns are read from the file.
+ * 'econtext' is used to evaluate default expression for each column that is
+ * either not read from the file or is using the DEFAULT option of COPY FROM.
+ * It can be NULL when no default values are used, i.e. when all columns are
+ * read from the file, and DEFAULT option is unset.
*
* 'values' and 'nulls' arrays must be the same length as columns of the
* relation passed to BeginCopyFrom. This function fills the arrays.
@@ -870,6 +871,7 @@ NextCopyFrom(CopyFromState cstate, ExprContext *econtext,
/* Initialize all values for row to NULL */
MemSet(values, 0, num_phys_attrs * sizeof(Datum));
MemSet(nulls, true, num_phys_attrs * sizeof(bool));
+ cstate->defaults = (bool *) palloc0(num_phys_attrs * sizeof(bool));
if (!cstate->opts.binary)
{
@@ -938,12 +940,27 @@ NextCopyFrom(CopyFromState cstate, ExprContext *econtext,
cstate->cur_attname = NameStr(att->attname);
cstate->cur_attval = string;
- values[m] = InputFunctionCall(&in_functions[m],
- string,
- typioparams[m],
- att->atttypmod);
+
if (string != NULL)
nulls[m] = false;
+
+ if (cstate->defaults[m])
+ {
+ /*
+ * The caller must supply econtext and have switched into the
+ * per-tuple memory context in it.
+ */
+ Assert(econtext != NULL);
+ Assert(CurrentMemoryContext == econtext->ecxt_per_tuple_memory);
+
+ values[m] = ExecEvalExpr(defexprs[m], econtext, &nulls[m]);
+ }
+ else
+ values[m] = InputFunctionCall(&in_functions[m],
+ string,
+ typioparams[m],
+ att->atttypmod);
+
cstate->cur_attname = NULL;
cstate->cur_attval = NULL;
}
@@ -1019,10 +1036,12 @@ NextCopyFrom(CopyFromState cstate, ExprContext *econtext,
Assert(econtext != NULL);
Assert(CurrentMemoryContext == econtext->ecxt_per_tuple_memory);
- values[defmap[i]] = ExecEvalExpr(defexprs[i], econtext,
+ values[defmap[i]] = ExecEvalExpr(defexprs[defmap[i]], econtext,
&nulls[defmap[i]]);
}
+ pfree(cstate->defaults);
+
return true;
}
@@ -1663,6 +1682,31 @@ CopyReadAttributesText(CopyFromState cstate)
if (input_len == cstate->opts.null_print_len &&
strncmp(start_ptr, cstate->opts.null_print, input_len) == 0)
cstate->raw_fields[fieldno] = NULL;
+ /* Check whether raw input matched default marker */
+ else if (cstate->opts.default_print &&
+ input_len == cstate->opts.default_print_len &&
+ strncmp(start_ptr, cstate->opts.default_print, input_len) == 0)
+ {
+ /* fieldno is 0-indexed and attnum is 1-indexed */
+ int m = list_nth_int(cstate->attnumlist, fieldno) - 1;
+
+ if (cstate->defexprs[m] != NULL)
+ {
+ /* defaults contain entries for all physical attributes */
+ cstate->defaults[m] = true;
+ }
+ else
+ {
+ TupleDesc tupDesc = RelationGetDescr(cstate->rel);
+ Form_pg_attribute att = TupleDescAttr(tupDesc, m);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+ errmsg("unexpected DEFAULT in COPY data"),
+ errdetail("Column \"%s\" has no DEFAULT value.",
+ NameStr(att->attname))));
+ }
+ }
else
{
/*
@@ -1852,6 +1896,31 @@ endfield:
if (!saw_quote && input_len == cstate->opts.null_print_len &&
strncmp(start_ptr, cstate->opts.null_print, input_len) == 0)
cstate->raw_fields[fieldno] = NULL;
+ /* Check whether raw input matched default marker */
+ else if (cstate->opts.default_print &&
+ input_len == cstate->opts.default_print_len &&
+ strncmp(start_ptr, cstate->opts.default_print, input_len) == 0)
+ {
+ /* fieldno is 0-index and attnum is 1-index */
+ int m = list_nth_int(cstate->attnumlist, fieldno) - 1;
+
+ if (cstate->defexprs[m] != NULL)
+ {
+ /* defaults contain entries for all physical attributes */
+ cstate->defaults[m] = true;
+ }
+ else
+ {
+ TupleDesc tupDesc = RelationGetDescr(cstate->rel);
+ Form_pg_attribute att = TupleDescAttr(tupDesc, m);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+ errmsg("unexpected DEFAULT in COPY data"),
+ errdetail("Column \"%s\" has no DEFAULT value.",
+ NameStr(att->attname))));
+ }
+ }
fieldno++;
/* Done if we hit EOL instead of a delim */
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 0167cb58a2..0f394420b2 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -325,4 +325,29 @@ is($row_count, '10',
'client-side error commits transaction, no ON_ERROR_STOP and multiple -c switches'
);
+# Test \copy from with DEFAULT option
+$node->safe_psql(
+ 'postgres',
+ "CREATE TABLE copy_default (
+ id integer PRIMARY KEY,
+ text_value text NOT NULL DEFAULT 'test',
+ ts_value timestamp without time zone NOT NULL DEFAULT '2022-07-05'
+ )"
+);
+
+my $copy_default_sql_file = "$tempdir/copy_default.csv";
+append_to_file($copy_default_sql_file, "1,value,2022-07-04\n");
+append_to_file($copy_default_sql_file, "2,placeholder,2022-07-03\n");
+append_to_file($copy_default_sql_file, "3,placeholder,placeholder\n");
+
+psql_like(
+ $node,
+ "\\copy copy_default from $copy_default_sql_file with (format 'csv', default 'placeholder');
+ SELECT * FROM copy_default",
+ qr/1\|value\|2022-07-04 00:00:00
+2|test|2022-07-03 00:00:00
+3|test|2022-07-05 00:00:00/,
+ '\copy from with DEFAULT'
+);
+
done_testing();
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 7746c75e0c..55a88f9812 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -442,6 +442,14 @@ check_completion("blarg \t\t", qr//, "check completion failure path");
clear_query();
+# check COPY FROM with DEFAULT option
+check_completion(
+ "COPY foo FROM stdin WITH ( DEF\t)",
+ qr/DEFAULT /,
+ "COPY FROM with DEFAULT completion");
+
+clear_line();
+
# send psql an explicit \q to shut it down, else pty won't close properly
$timer->start($PostgreSQL::Test::Utils::timeout_default);
$in .= "\\q\n";
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8f12af799b..42e87b9e49 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2857,7 +2857,7 @@ psql_completion(const char *text, int start, int end)
else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "("))
COMPLETE_WITH("FORMAT", "FREEZE", "DELIMITER", "NULL",
"HEADER", "QUOTE", "ESCAPE", "FORCE_QUOTE",
- "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING");
+ "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING", "DEFAULT");
/* Complete COPY FROM|TO filename WITH (FORMAT */
else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT"))
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8e5f6ff148..33175868f6 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -47,6 +47,8 @@ typedef struct CopyFormatOptions
char *null_print; /* NULL marker string (server encoding!) */
int null_print_len; /* length of same */
char *null_print_client; /* same converted to file encoding */
+ char *default_print; /* DEFAULT marker string */
+ int default_print_len; /* length of same */
char *delim; /* column delimiter (must be 1 byte) */
char *quote; /* CSV quote char (must be 1 byte) */
char *escape; /* CSV escape char (must be 1 byte) */
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 7b1c4327bd..ac2c16f8b8 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -44,8 +44,7 @@ typedef enum EolType
*/
typedef enum CopyInsertMethod
{
- CIM_SINGLE, /* use table_tuple_insert or
- * ExecForeignInsert */
+ CIM_SINGLE, /* use table_tuple_insert or ExecForeignInsert */
CIM_MULTI, /* always use table_multi_insert or
* ExecForeignBatchInsert */
CIM_MULTI_CONDITIONAL /* use table_multi_insert or
@@ -91,11 +90,16 @@ typedef struct CopyFromStateData
*/
MemoryContext copycontext; /* per-copy execution context */
- AttrNumber num_defaults;
+ AttrNumber num_defaults; /* count of att that are missing and have
+ * default value */
FmgrInfo *in_functions; /* array of input functions for each attrs */
Oid *typioparams; /* array of element types for in_functions */
- int *defmap; /* array of default att numbers */
- ExprState **defexprs; /* array of default att expressions */
+ int *defmap; /* array of default att numbers related to
+ * missing att */
+ ExprState **defexprs; /* array of default att expressions for all
+ * att */
+ bool *defaults; /* if DEFAULT marker was found for
+ * corresponding att */
bool volatile_defexprs; /* is any of defexprs volatile? */
List *range_table; /* single element list of RangeTblEntry */
List *rteperminfos; /* single element list of RTEPermissionInfo */
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 090ef6c7a8..8e33eee719 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -680,3 +680,101 @@ DROP TABLE instead_of_insert_tbl;
DROP VIEW instead_of_insert_tbl_view;
DROP VIEW instead_of_insert_tbl_view_2;
DROP FUNCTION fun_instead_of_insert_tbl();
+--
+-- COPY FROM ... DEFAULT
+--
+create temp table copy_default (
+ id integer primary key,
+ text_value text not null default 'test',
+ ts_value timestamp without time zone not null default '2022-07-05'
+);
+-- if DEFAULT is not specified, then the marker will be regular data
+copy copy_default from stdin;
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | value | Mon Jul 04 00:00:00 2022
+ 2 | D | Tue Jul 05 00:00:00 2022
+(2 rows)
+
+truncate copy_default;
+copy copy_default from stdin with (format csv);
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | value | Mon Jul 04 00:00:00 2022
+ 2 | \D | Tue Jul 05 00:00:00 2022
+(2 rows)
+
+truncate copy_default;
+-- DEFAULT cannot be used in binary mode
+copy copy_default from stdin with (format binary, default '\D');
+ERROR: cannot specify DEFAULT in BINARY mode
+-- DEFAULT cannot be new line nor carriage return
+copy copy_default from stdin with (default E'\n');
+ERROR: COPY default representation cannot use newline or carriage return
+copy copy_default from stdin with (default E'\r');
+ERROR: COPY default representation cannot use newline or carriage return
+-- DELIMITER cannot appear in DEFAULT spec
+copy copy_default from stdin with (delimiter ';', default 'test;test');
+ERROR: COPY delimiter must not appear in the DEFAULT specification
+-- CSV quote cannot appear in DEFAULT spec
+copy copy_default from stdin with (format csv, quote '"', default 'test"test');
+ERROR: CSV quote character must not appear in the DEFAULT specification
+-- NULL and DEFAULT spec must be different
+copy copy_default from stdin with (default '\N');
+ERROR: NULL specification and DEFAULT specification cannot be the same
+-- cannot use DEFAULT marker in column that has no DEFAULT value
+copy copy_default from stdin with (default '\D');
+ERROR: unexpected DEFAULT in COPY data
+DETAIL: Column "id" has no DEFAULT value.
+CONTEXT: COPY copy_default, line 1: "\D value '2022-07-04'"
+copy copy_default from stdin with (format csv, default '\D');
+ERROR: unexpected DEFAULT in COPY data
+DETAIL: Column "id" has no DEFAULT value.
+CONTEXT: COPY copy_default, line 1: "\D,value,2022-07-04"
+-- The DEFAULT marker must be unquoted and unescaped or it's not recognized
+copy copy_default from stdin with (default '\D');
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | test | Mon Jul 04 00:00:00 2022
+ 2 | \D | Mon Jul 04 00:00:00 2022
+ 3 | "D" | Mon Jul 04 00:00:00 2022
+(3 rows)
+
+truncate copy_default;
+copy copy_default from stdin with (format csv, default '\D');
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | test | Mon Jul 04 00:00:00 2022
+ 2 | \\D | Mon Jul 04 00:00:00 2022
+ 3 | \D | Mon Jul 04 00:00:00 2022
+(3 rows)
+
+truncate copy_default;
+-- successful usage of DEFAULT option in COPY
+copy copy_default from stdin with (default '\D');
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | value | Mon Jul 04 00:00:00 2022
+ 2 | test | Sun Jul 03 00:00:00 2022
+ 3 | test | Tue Jul 05 00:00:00 2022
+(3 rows)
+
+truncate copy_default;
+copy copy_default from stdin with (format csv, default '\D');
+select id, text_value, ts_value from copy_default;
+ id | text_value | ts_value
+----+------------+--------------------------
+ 1 | value | Mon Jul 04 00:00:00 2022
+ 2 | test | Sun Jul 03 00:00:00 2022
+ 3 | test | Tue Jul 05 00:00:00 2022
+(3 rows)
+
+truncate copy_default;
+-- DEFAULT cannot be used in COPY TO
+copy (select 1 as test) TO stdout with (default '\D');
+ERROR: COPY DEFAULT only available using COPY FROM
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index b0de82c3aa..d759635068 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -478,3 +478,104 @@ DROP TABLE instead_of_insert_tbl;
DROP VIEW instead_of_insert_tbl_view;
DROP VIEW instead_of_insert_tbl_view_2;
DROP FUNCTION fun_instead_of_insert_tbl();
+
+--
+-- COPY FROM ... DEFAULT
+--
+
+create temp table copy_default (
+ id integer primary key,
+ text_value text not null default 'test',
+ ts_value timestamp without time zone not null default '2022-07-05'
+);
+
+-- if DEFAULT is not specified, then the marker will be regular data
+copy copy_default from stdin;
+1 value '2022-07-04'
+2 \D '2022-07-05'
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+copy copy_default from stdin with (format csv);
+1,value,2022-07-04
+2,\D,2022-07-05
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+-- DEFAULT cannot be used in binary mode
+copy copy_default from stdin with (format binary, default '\D');
+
+-- DEFAULT cannot be new line nor carriage return
+copy copy_default from stdin with (default E'\n');
+copy copy_default from stdin with (default E'\r');
+
+-- DELIMITER cannot appear in DEFAULT spec
+copy copy_default from stdin with (delimiter ';', default 'test;test');
+
+-- CSV quote cannot appear in DEFAULT spec
+copy copy_default from stdin with (format csv, quote '"', default 'test"test');
+
+-- NULL and DEFAULT spec must be different
+copy copy_default from stdin with (default '\N');
+
+-- cannot use DEFAULT marker in column that has no DEFAULT value
+copy copy_default from stdin with (default '\D');
+\D value '2022-07-04'
+2 \D '2022-07-05'
+\.
+
+copy copy_default from stdin with (format csv, default '\D');
+\D,value,2022-07-04
+2,\D,2022-07-05
+\.
+
+-- The DEFAULT marker must be unquoted and unescaped or it's not recognized
+copy copy_default from stdin with (default '\D');
+1 \D '2022-07-04'
+2 \\D '2022-07-04'
+3 "\D" '2022-07-04'
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+copy copy_default from stdin with (format csv, default '\D');
+1,\D,2022-07-04
+2,\\D,2022-07-04
+3,"\D",2022-07-04
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+-- successful usage of DEFAULT option in COPY
+copy copy_default from stdin with (default '\D');
+1 value '2022-07-04'
+2 \D '2022-07-03'
+3 \D \D
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+copy copy_default from stdin with (format csv, default '\D');
+1,value,2022-07-04
+2,\D,2022-07-03
+3,\D,\D
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+-- DEFAULT cannot be used in COPY TO
+copy (select 1 as test) TO stdout with (default '\D');