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 <replaceable class="parameter">count</replaceable>
     with views.  However, you can write <literal>COPY (SELECT * FROM
     <replaceable class="parameter">viewname</replaceable>) TO ...</literal>.
    </para>
+   
+   <para>
+    <command>COPY FROM</command> can only be used with plain tables and views
+    with INSTEAD of INSERT triggers.
+   </para>
 
    <para>
     <command>COPY</command> 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();
