From f114ff866f4f650519c772072e6d1c68af08f0e9 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" 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 Reviewed-by: Kyotaro Horiguchi 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)