From 2ecb574cdd1d63dc9b935f9f20f116ec588eb0c2 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" 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 --- 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)