From 2ecb574cdd1d63dc9b935f9f20f116ec588eb0c2 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 v1] Reject unsupported COPY FROM targets before WHERE checks

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

Move the target-relation precheck into a separate PrecheckCopyFrom()
function and call it earlier from DoCopy(), before processing the WHERE
clause. Keep a do_precheck option on CopyFrom() so direct callers can still
request the same validation.

This makes commands such as COPY FROM a sequence fail immediately with
"cannot copy to sequence", instead of first reporting errors from the WHERE
clause or waiting for COPY input.

Author: Chao Li <lic@highgo.com>
---
 src/backend/commands/copy.c                 | 11 ++-
 src/backend/commands/copyfrom.c             | 78 ++++++++++++---------
 src/backend/replication/logical/tablesync.c |  2 +-
 src/include/commands/copy.h                 |  3 +-
 src/test/regress/expected/copy2.out         |  5 ++
 src/test/regress/sql/copy2.sql              |  7 +-
 6 files changed, 67 insertions(+), 39 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 003b70852bb..464b1ec0430 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -132,6 +132,9 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		perminfo = nsitem->p_perminfo;
 		perminfo->requiredPerms = (is_from ? ACL_INSERT : ACL_SELECT);
 
+		if (is_from)
+			PrecheckCopyFrom(rel);
+
 		if (stmt->whereClause)
 		{
 			Bitmapset  *expr_attrs = NULL;
@@ -367,7 +370,13 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		cstate = BeginCopyFrom(pstate, rel, whereClause,
 							   stmt->filename, stmt->is_program,
 							   NULL, stmt->attlist, stmt->options);
-		*processed = CopyFrom(cstate);	/* copy from file to database */
+
+		/*
+		 * Copy from file to database. The target relation was already checked
+		 * above, before processing the WHERE clause, so CopyFrom can skip the
+		 * precheck.
+		 */
+		*processed = CopyFrom(cstate, false);
 		EndCopyFrom(cstate);
 	}
 	else
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 0087585b2c4..b7d63612b66 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -774,11 +774,52 @@ CopyMultiInsertInfoStore(CopyMultiInsertInfo *miinfo, ResultRelInfo *rri,
 	miinfo->bufferedBytes += tuplen;
 }
 
+/*
+ * Check whether the target relation can be used in COPY FROM.
+ */
+void
+PrecheckCopyFrom(Relation rel)
+{
+	/*
+	 * 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))));
+	}
+}
+
 /*
  * Copy FROM file to relation.
  */
 uint64
-CopyFrom(CopyFromState cstate)
+CopyFrom(CopyFromState cstate, bool do_precheck)
 {
 	ResultRelInfo *resultRelInfo;
 	ResultRelInfo *target_resultRelInfo;
@@ -808,39 +849,8 @@ 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 (do_precheck)
+		PrecheckCopyFrom(cstate->rel);
 
 	/*
 	 * If the target file is new-in-transaction, we assume that checking FSM
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index a04b84ebc1d..7b04fda92ef 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1209,7 +1209,7 @@ copy_table(Relation rel)
 	cstate = BeginCopyFrom(pstate, rel, NULL, NULL, false, copy_read_data, attnamelist, options);
 
 	/* Do the copy */
-	(void) CopyFrom(cstate);
+	(void) CopyFrom(cstate, true);
 	EndCopyFrom(cstate);
 
 	logicalrep_rel_close(relmapentry, NoLock);
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index abecfe51098..f0d68fc55c3 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -115,6 +115,7 @@ extern CopyFromState BeginCopyFrom(ParseState *pstate, Relation rel, Node *where
 								   const char *filename,
 								   bool is_program, copy_data_source_cb data_source_cb, List *attnamelist, List *options);
 extern void EndCopyFrom(CopyFromState cstate);
+extern void PrecheckCopyFrom(Relation rel);
 extern bool NextCopyFrom(CopyFromState cstate, ExprContext *econtext,
 						 Datum *values, bool *nulls);
 extern bool NextCopyFromRawFields(CopyFromState cstate,
@@ -122,7 +123,7 @@ extern bool NextCopyFromRawFields(CopyFromState cstate,
 extern void CopyFromErrorCallback(void *arg);
 extern char *CopyLimitPrintoutLength(const char *str);
 
-extern uint64 CopyFrom(CopyFromState cstate);
+extern uint64 CopyFrom(CopyFromState cstate, bool do_precheck);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 919eabd5f78..b8420b93d2c 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -207,6 +207,11 @@ LINE 1: COPY x from stdin WHERE a = row_number() over(b);
 COPY x from stdin WHERE tableoid = 'x'::regclass;
 ERROR:  system columns are not supported in COPY FROM WHERE conditions
 DETAIL:  Column "tableoid" is a system column.
+-- check that COPY FROM rejects an invalid target before analyzing WHERE.
+CREATE SEQUENCE copy_seq;
+COPY copy_seq FROM stdin WHERE tableoid IS NULL;
+ERROR:  cannot copy to sequence "copy_seq"
+DROP SEQUENCE copy_seq;
 -- check results of copy in
 SELECT * FROM x;
    a   | b  |     c      |   d    |          e           
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index f853499021d..d9e2562b028 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -170,6 +170,11 @@ COPY x from stdin WHERE a = row_number() over(b);
 
 COPY x from stdin WHERE tableoid = 'x'::regclass;
 
+-- check that COPY FROM rejects an invalid target before analyzing WHERE.
+CREATE SEQUENCE copy_seq;
+COPY copy_seq FROM stdin WHERE tableoid IS NULL;
+DROP SEQUENCE copy_seq;
+
 
 -- check results of copy in
 SELECT * FROM x;
@@ -485,8 +490,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)

