From f114ff866f4f650519c772072e6d1c68af08f0e9 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Wed, 24 Jun 2026 15:25:17 +0800
Subject: [PATCH v2] Report unsupported COPY FROM targets before reading input

COPY FROM rejected unsupported target relations, such as sequences, inside
CopyFrom(). For COPY FROM STDIN, that is too late: the command may already
have entered COPY input mode before reporting the target-relation error.

Move the target-relation check to BeginCopyFrom(), before the command starts
reading input. This keeps the existing precedence of earlier parse analysis,
permission, and WHERE-clause errors, while making COPY FROM consistent with
COPY TO, where the corresponding relation-kind checks are performed in
BeginCopyTo().

This makes commands such as COPY FROM a sequence fail immediately with
"cannot copy to sequence", instead of waiting for COPY input first.

Author: Chao Li <lic@highgo.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Discussion: https://postgr.es/m/84873FFF-4C50-4C85-9A36-62A01B860FF1@gmail.com
---
 src/backend/commands/copyfrom.c | 68 ++++++++++++++++-----------------
 src/test/regress/sql/copy2.sql  |  2 -
 2 files changed, 34 insertions(+), 36 deletions(-)

diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 0087585b2c4..9a26599ba20 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -808,40 +808,6 @@ CopyFrom(CopyFromState cstate)
 	if (cstate->opts.on_error != COPY_ON_ERROR_STOP)
 		Assert(cstate->escontext);
 
-	/*
-	 * The target must be a plain, foreign, or partitioned relation, or have
-	 * an INSTEAD OF INSERT row trigger.  (Currently, such triggers are only
-	 * allowed on views, so we only hint about them in the view case.)
-	 */
-	if (cstate->rel->rd_rel->relkind != RELKIND_RELATION &&
-		cstate->rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
-		cstate->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
-		!(cstate->rel->trigdesc &&
-		  cstate->rel->trigdesc->trig_insert_instead_row))
-	{
-		if (cstate->rel->rd_rel->relkind == RELKIND_VIEW)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot copy to view \"%s\"",
-							RelationGetRelationName(cstate->rel)),
-					 errhint("To enable copying to a view, provide an INSTEAD OF INSERT trigger.")));
-		else if (cstate->rel->rd_rel->relkind == RELKIND_MATVIEW)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot copy to materialized view \"%s\"",
-							RelationGetRelationName(cstate->rel))));
-		else if (cstate->rel->rd_rel->relkind == RELKIND_SEQUENCE)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot copy to sequence \"%s\"",
-							RelationGetRelationName(cstate->rel))));
-		else
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("cannot copy to non-table relation \"%s\"",
-							RelationGetRelationName(cstate->rel))));
-	}
-
 	/*
 	 * If the target file is new-in-transaction, we assume that checking FSM
 	 * for free space is a waste of time.  This could possibly be wrong, but
@@ -1563,6 +1529,40 @@ BeginCopyFrom(ParseState *pstate,
 		0
 	};
 
+	/*
+	 * The target must be a plain, foreign, or partitioned relation, or have
+	 * an INSTEAD OF INSERT row trigger.  (Currently, such triggers are only
+	 * allowed on views, so we only hint about them in the view case.)
+	 */
+	if (rel->rd_rel->relkind != RELKIND_RELATION &&
+		rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
+		!(rel->trigdesc &&
+		  rel->trigdesc->trig_insert_instead_row))
+	{
+		if (rel->rd_rel->relkind == RELKIND_VIEW)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot copy to view \"%s\"",
+							RelationGetRelationName(rel)),
+					 errhint("To enable copying to a view, provide an INSTEAD OF INSERT trigger.")));
+		else if (rel->rd_rel->relkind == RELKIND_MATVIEW)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot copy to materialized view \"%s\"",
+							RelationGetRelationName(rel))));
+		else if (rel->rd_rel->relkind == RELKIND_SEQUENCE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot copy to sequence \"%s\"",
+							RelationGetRelationName(rel))));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot copy to non-table relation \"%s\"",
+							RelationGetRelationName(rel))));
+	}
+
 	/* Allocate workspace and zero all fields */
 	cstate = palloc0_object(CopyFromStateData);
 
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index f853499021d..ca31cff80f9 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -485,8 +485,6 @@ CREATE TABLE instead_of_insert_tbl(id serial, name text);
 CREATE VIEW instead_of_insert_tbl_view AS SELECT ''::text AS str;
 
 COPY instead_of_insert_tbl_view FROM stdin; -- fail
-test1
-\.
 
 CREATE FUNCTION fun_instead_of_insert_tbl() RETURNS trigger AS $$
 BEGIN
-- 
2.50.1 (Apple Git-155)

