From 28ff0866e8c1eb0dc756d0f4bb9d5f6ac9d1519a Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Fri, 15 May 2026 09:00:28 +0800
Subject: [PATCH v1] Fix ON_ERROR SET_NULL domain check with reordered COPY
 column lists

BeginCopyFrom() initialized domain_with_constraint using positions in the
COPY column list, but CopyFromTextLikeOneRow() checks it using physical
attribute numbers.  With a COPY column list whose order differs from the
table's physical column order, this could miss a domain constraint or read
the wrong flag.

Allocate the array for all physical attributes and store each flag using
attnum - 1, matching the later lookup.

While here, also change a few palloc to palloc_array.

Author: Chao Li <lic@highgo.com>
---
 src/backend/commands/copyfrom.c     | 22 ++++++++++------------
 src/test/regress/expected/copy2.out |  7 +++++++
 src/test/regress/sql/copy2.sql      |  7 +++++++
 3 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 64ac3063c61..eddc46f09e1 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1636,8 +1636,6 @@ BeginCopyFrom(ParseState *pstate,
 
 	if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL)
 	{
-		int			attr_count = list_length(cstate->attnumlist);
-
 		/*
 		 * When data type conversion fails and ON_ERROR is SET_NULL, we need
 		 * ensure that the input column allow null values.  ExecConstraints()
@@ -1646,20 +1644,20 @@ BeginCopyFrom(ParseState *pstate,
 		 * check must be performed during the initial string-to-datum
 		 * conversion (see CopyFromTextLikeOneRow()).
 		 */
-		cstate->domain_with_constraint = palloc0_array(bool, attr_count);
+		cstate->domain_with_constraint = palloc0_array(bool, num_phys_attrs);
 
 		foreach_int(attno, cstate->attnumlist)
 		{
-			int			i = foreach_current_index(attno);
+			int			m = attno - 1;
 
-			Form_pg_attribute att = TupleDescAttr(tupDesc, attno - 1);
+			Form_pg_attribute att = TupleDescAttr(tupDesc, m);
 
-			cstate->domain_with_constraint[i] = DomainHasConstraints(att->atttypid, NULL);
+			cstate->domain_with_constraint[m] = DomainHasConstraints(att->atttypid, NULL);
 		}
 	}
 
 	/* 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 = palloc0_array(bool, num_phys_attrs);
 	if (cstate->opts.force_null_all)
 		MemSet(cstate->opts.force_null_flags, true, num_phys_attrs * sizeof(bool));
 	else if (cstate->opts.force_null)
@@ -1777,10 +1775,10 @@ BeginCopyFrom(ParseState *pstate,
 	 * the input function), and info about defaults and constraints. (Which
 	 * input function we use depends on text/binary format choice.)
 	 */
-	in_functions = (FmgrInfo *) palloc(num_phys_attrs * sizeof(FmgrInfo));
-	typioparams = (Oid *) palloc(num_phys_attrs * sizeof(Oid));
-	defmap = (int *) palloc(num_phys_attrs * sizeof(int));
-	defexprs = (ExprState **) palloc(num_phys_attrs * sizeof(ExprState *));
+	in_functions = palloc_array(FmgrInfo, num_phys_attrs);
+	typioparams = palloc_array(Oid, num_phys_attrs);
+	defmap = palloc_array(int, num_phys_attrs);
+	defexprs = palloc_array(ExprState *, num_phys_attrs);
 
 	for (int attnum = 1; attnum <= num_phys_attrs; attnum++)
 	{
@@ -1846,7 +1844,7 @@ BeginCopyFrom(ParseState *pstate,
 		}
 	}
 
-	cstate->defaults = (bool *) palloc0(tupDesc->natts * sizeof(bool));
+	cstate->defaults = palloc0_array(bool, tupDesc->natts);
 
 	/* initialize progress */
 	pgstat_progress_start_command(PROGRESS_COMMAND_COPY,
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 7600e5239d2..5ec2ad9f156 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -800,6 +800,7 @@ NOTICE:  6 rows were skipped due to data type incompatibility
 CREATE DOMAIN d_int_not_null AS integer NOT NULL CHECK (value > 0);
 CREATE DOMAIN d_int_positive_maybe_null AS integer CHECK (value > 0);
 CREATE TABLE t_on_error_null (a d_int_not_null, b d_int_positive_maybe_null, c integer);
+CREATE TABLE t_on_error_null_col_order (a integer, b d_int_not_null, c integer);
 \pset null NULL
 COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail
 ERROR:  domain d_int_not_null does not allow null values
@@ -813,6 +814,11 @@ COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail
 ERROR:  domain d_int_not_null does not allow null values
 DETAIL:  ON_ERROR SET_NULL cannot be applied because column "a" (domain d_int_not_null) does not accept null values.
 CONTEXT:  COPY t_on_error_null, line 1, column a: "-1"
+-- fail, COPY column list order is not physical attribute order.
+COPY t_on_error_null_col_order (b, c) FROM STDIN WITH (on_error set_null);
+ERROR:  domain d_int_not_null does not allow null values
+DETAIL:  ON_ERROR SET_NULL cannot be applied because column "b" (domain d_int_not_null) does not accept null values.
+CONTEXT:  COPY t_on_error_null_col_order, line 1, column b: "ss"
 -- fail, less data.
 COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null);
 ERROR:  missing data for column "c"
@@ -899,6 +905,7 @@ DROP VIEW instead_of_insert_tbl_view;
 DROP VIEW instead_of_insert_tbl_view_2;
 DROP FUNCTION fun_instead_of_insert_tbl();
 DROP TABLE check_ign_err;
+DROP TABLE t_on_error_null_col_order;
 DROP TABLE t_on_error_null;
 DROP DOMAIN d_int_not_null;
 DROP DOMAIN d_int_positive_maybe_null;
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index e0810109473..3be2fdb9d42 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -549,6 +549,7 @@ a	{2}	2
 CREATE DOMAIN d_int_not_null AS integer NOT NULL CHECK (value > 0);
 CREATE DOMAIN d_int_positive_maybe_null AS integer CHECK (value > 0);
 CREATE TABLE t_on_error_null (a d_int_not_null, b d_int_positive_maybe_null, c integer);
+CREATE TABLE t_on_error_null_col_order (a integer, b d_int_not_null, c integer);
 
 \pset null NULL
 COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail
@@ -563,6 +564,11 @@ COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail
 -1	11	13
 \.
 
+-- fail, COPY column list order is not physical attribute order.
+COPY t_on_error_null_col_order (b, c) FROM STDIN WITH (on_error set_null);
+ss	1
+\.
+
 -- fail, less data.
 COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null);
 1,1
@@ -651,6 +657,7 @@ DROP VIEW instead_of_insert_tbl_view;
 DROP VIEW instead_of_insert_tbl_view_2;
 DROP FUNCTION fun_instead_of_insert_tbl();
 DROP TABLE check_ign_err;
+DROP TABLE t_on_error_null_col_order;
 DROP TABLE t_on_error_null;
 DROP DOMAIN d_int_not_null;
 DROP DOMAIN d_int_positive_maybe_null;
-- 
2.50.1 (Apple Git-155)

