From a9ea51eaa4330786e35c1da224fc0659bf27aee8 Mon Sep 17 00:00:00 2001
From: AyoubKAZ <kazarayoub2004@gmail.com>
Date: Wed, 4 Nov 2026 00:57:14 +0100
Subject: [PATCH] Fix DEFAULT marker handling in COPY FROM ... HEADER MATCH

    When using COPY FROM with both HEADER MATCH and DEFAULT options,
    the header line is parsed using CopyReadAttributesText() or
    CopyReadAttributesCSV().  Because default marker matching was active
    during header line parsing, any header column name identical to the
    DEFAULT marker string was mistakenly processed as a data default marker.

    This caused two bugs depending on whether the column had a default:

    1. If the column had no default expression, parsing the
       header line raised an error ("unexpected default marker in COPY data")

    2. If the column had a default expression, CopyReadAttributes*() set cstate->defaults[m] = true while reading
       the header.  Because header parsing and the first data row are
       processed in the same NextCopyFrom() call, and cstate->defaults is
       cleared only at the start of NextCopyFrom(), the flag set during header
       parsing just followed up into the first actual row .  As a result, the input
       value in the first row was discarded and replaced with the column's
       default value, while subsequent data rows were unaffected.

    Fix by temporarily disabling default marker recognition while parsing
    the header line under HEADER MATCH.
---
 src/backend/commands/copyfromparse.c | 10 ++++++++++
 src/test/regress/expected/copy2.out  | 18 ++++++++++++++++++
 src/test/regress/sql/copy2.sql       | 20 ++++++++++++++++++++
 3 files changed, 48 insertions(+)

diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index 06450535779..4cc656117f3 100644
--- a/src/backend/commands/copyfromparse.c
+++ b/src/backend/commands/copyfromparse.c
@@ -831,12 +831,22 @@ NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields
 		if (cstate->opts.header_line == COPY_HEADER_MATCH)
 		{
 			int			fldnum;
+			char	   *save_default_print = cstate->opts.default_print;
+
+			/*
+			 * Disable DEFAULT marker recognition while parsing the header
+			 * line, so that column names matching the default string are not
+			 * treated as default markers.
+			 */
+			cstate->opts.default_print = NULL;
 
 			if (is_csv)
 				fldct = CopyReadAttributesCSV(cstate);
 			else
 				fldct = CopyReadAttributesText(cstate);
 
+			cstate->opts.default_print = save_default_print;
+
 			if (fldct != list_length(cstate->attnumlist))
 				ereport(ERROR,
 						(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index df6fcd91799..57523d8de9c 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -1008,6 +1008,24 @@ select id, text_value, ts_value from copy_default;
   3 | test       | Tue Jul 05 00:00:00 2022
 (3 rows)
 
+truncate copy_default;
+-- DEFAULT with HEADER MATCH where header field matches DEFAULT marker
+copy copy_default from stdin with (format csv, header match, default 'id');
+select id, text_value, ts_value from copy_default;
+ id | text_value |         ts_value         
+----+------------+--------------------------
+  1 | hello      | Mon Jul 04 00:00:00 2022
+(1 row)
+
+truncate copy_default;
+copy copy_default from stdin with (format csv, header match, default 'text_value');
+select id, text_value, ts_value from copy_default;
+ id | text_value |         ts_value         
+----+------------+--------------------------
+  1 | hello      | Mon Jul 04 00:00:00 2022
+  2 | test       | Sun Jul 03 00:00:00 2022
+(2 rows)
+
 truncate copy_default;
 -- DEFAULT cannot be used in COPY TO
 copy (select 1 as test) TO stdout with (default '\D');
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index ec6f5e8cfdc..dc31137e51b 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -819,5 +819,25 @@ select id, text_value, ts_value from copy_default;
 
 truncate copy_default;
 
+-- DEFAULT with HEADER MATCH where header field matches DEFAULT marker
+copy copy_default from stdin with (format csv, header match, default 'id');
+id,text_value,ts_value
+1,hello,2022-07-04
+\.
+
+select id, text_value, ts_value from copy_default;
+
+truncate copy_default;
+
+copy copy_default from stdin with (format csv, header match, default 'text_value');
+id,text_value,ts_value
+1,hello,2022-07-04
+2,text_value,2022-07-03
+\.
+
+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');
-- 
2.34.1

