diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 07e2f45..4d13478 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -399,6 +399,11 @@ COPY count with views. However, you can write COPY (SELECT * FROM viewname) TO .... + + + COPY FROM can only be used with plain tables and views + with INSTEAD of INSERT triggers. + COPY only deals with the specific table named; diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index b4140eb..9c9c8c0 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -2269,13 +2269,23 @@ CopyFrom(CopyState cstate) Assert(cstate->rel); - if (cstate->rel->rd_rel->relkind != RELKIND_RELATION) + /* + * Check whether the target RELKIND is not a RELATION and raise an error + * if the relation doesn't contains any INSTEAD of triggers. Creation of + * INSTEAD triggers are possible only on VIEWs, check CreateTrigger + * function in trigger.c file. + */ + if (cstate->rel->rd_rel->relkind != RELKIND_RELATION && + (!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)))); + RelationGetRelationName(cstate->rel)), + errhint("To enable copy to view, provide" + " an INSTEAD OF INSERT trigger"))); else if (cstate->rel->rd_rel->relkind == RELKIND_MATVIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -2496,52 +2506,61 @@ CopyFrom(CopyState cstate) if (!skip_tuple) { - /* Check the constraints of the tuple */ - if (cstate->rel->rd_att->constr) - ExecConstraints(resultRelInfo, slot, estate); - - if (useHeapMultiInsert) + /* INSTEAD ROW INSERT Triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_insert_instead_row) { - /* Add this tuple to the tuple buffer */ - if (nBufferedTuples == 0) - firstBufferedLineNo = cstate->cur_lineno; - bufferedTuples[nBufferedTuples++] = tuple; - bufferedTuplesSize += tuple->t_len; - - /* - * If the buffer filled up, flush it. Also flush if the total - * size of all the tuples in the buffer becomes large, to - * avoid using large amounts of memory for the buffers when - * the tuples are exceptionally wide. - */ - if (nBufferedTuples == MAX_BUFFERED_TUPLES || - bufferedTuplesSize > 65535) - { - CopyFromInsertBatch(cstate, estate, mycid, hi_options, - resultRelInfo, myslot, bistate, - nBufferedTuples, bufferedTuples, - firstBufferedLineNo); - nBufferedTuples = 0; - bufferedTuplesSize = 0; - } + ExecIRInsertTriggers(estate, resultRelInfo, slot); } else { - List *recheckIndexes = NIL; + /* Check the constraints of the tuple */ + if (cstate->rel->rd_att->constr) + ExecConstraints(resultRelInfo, slot, estate); + + if (useHeapMultiInsert) + { + /* Add this tuple to the tuple buffer */ + if (nBufferedTuples == 0) + firstBufferedLineNo = cstate->cur_lineno; + bufferedTuples[nBufferedTuples++] = tuple; + bufferedTuplesSize += tuple->t_len; + + /* + * If the buffer filled up, flush it. Also flush if the + * total size of all the tuples in the buffer becomes + * large, to avoid using large amounts of memory for the + * buffers when the tuples are exceptionally wide. + */ + if (nBufferedTuples == MAX_BUFFERED_TUPLES || + bufferedTuplesSize > 65535) + { + CopyFromInsertBatch(cstate, estate, mycid, hi_options, + resultRelInfo, myslot, bistate, + nBufferedTuples, bufferedTuples, + firstBufferedLineNo); + nBufferedTuples = 0; + bufferedTuplesSize = 0; + } + } + else + { + List *recheckIndexes = NIL; - /* OK, store the tuple and create index entries for it */ - heap_insert(cstate->rel, tuple, mycid, hi_options, bistate); + /* OK, store the tuple and create index entries for it */ + heap_insert(cstate->rel, tuple, mycid, hi_options, bistate); - if (resultRelInfo->ri_NumIndices > 0) - recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), + if (resultRelInfo->ri_NumIndices > 0) + recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self), estate, false, NULL, - NIL); + NIL); - /* AFTER ROW INSERT Triggers */ - ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + /* AFTER ROW INSERT Triggers */ + ExecARInsertTriggers(estate, resultRelInfo, tuple, + recheckIndexes); - list_free(recheckIndexes); + list_free(recheckIndexes); + } } /* diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 9a8922d..d0b6029 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -535,6 +535,30 @@ COPY rls_t1 (a, b) TO stdout; 2 3 4 1 RESET SESSION AUTHORIZATION; +-- test with INSTEAD OF INSERT trigger on a view +CREATE TABLE instead_of_insert_tbl(id serial, name text); +CREATE VIEW instead_of_insert_tbl_view AS SELECT ''::text AS str; +CREATE FUNCTION fun_instead_of_insert_tbl() RETURNS trigger AS $fun_instead_of_insert_tbl$ +BEGIN +INSERT INTO instead_of_insert_tbl (name) VALUES (NEW.str); +RETURN NULL; +END; +$fun_instead_of_insert_tbl$ LANGUAGE plpgsql; +COPY instead_of_insert_tbl_view FROM stdin; -- failure case +ERROR: cannot copy to view "instead_of_insert_tbl_view" +HINT: To enable copy to view, provide an INSTEAD OF INSERT trigger +CREATE TRIGGER trig_instead_of_insert_tbl_view INSTEAD OF INSERT ON instead_of_insert_tbl_view FOR EACH ROW EXECUTE +PROCEDURE fun_instead_of_insert_tbl(); +COPY instead_of_insert_tbl_view FROM stdin; +SELECT * from instead_of_insert_tbl; + id | name +----+------- + 1 | test1 +(1 row) + +DROP FUNCTION fun_instead_of_insert_tbl() CASCADE; +NOTICE: drop cascades to trigger trig_instead_of_insert_tbl_view on view instead_of_insert_tbl_view +DROP TABLE instead_of_insert_tbl CASCADE; DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact(); diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql index 89d0a39..f8aa7e4 100644 --- a/src/test/regress/sql/copy2.sql +++ b/src/test/regress/sql/copy2.sql @@ -387,6 +387,31 @@ COPY rls_t1 (a, b) TO stdout; RESET SESSION AUTHORIZATION; +-- test with INSTEAD OF INSERT trigger on a view +CREATE TABLE instead_of_insert_tbl(id serial, name text); +CREATE VIEW instead_of_insert_tbl_view AS SELECT ''::text AS str; +CREATE FUNCTION fun_instead_of_insert_tbl() RETURNS trigger AS $fun_instead_of_insert_tbl$ +BEGIN +INSERT INTO instead_of_insert_tbl (name) VALUES (NEW.str); +RETURN NULL; +END; +$fun_instead_of_insert_tbl$ LANGUAGE plpgsql; + +COPY instead_of_insert_tbl_view FROM stdin; -- failure case +test1 +\. + +CREATE TRIGGER trig_instead_of_insert_tbl_view INSTEAD OF INSERT ON instead_of_insert_tbl_view FOR EACH ROW EXECUTE +PROCEDURE fun_instead_of_insert_tbl(); +COPY instead_of_insert_tbl_view FROM stdin; +test1 +\. + +SELECT * from instead_of_insert_tbl; + +DROP FUNCTION fun_instead_of_insert_tbl() CASCADE; +DROP TABLE instead_of_insert_tbl CASCADE; + DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact();