Executing an INSERT statement on a View in linked server
Consider this scenario where you have a linked server from one SQL server to another SQL Server. Both the SQL Servers are SQL Server 2008 SP2 on Windows 2008.
Say the two SQL Servers are Server A and Server B.
Linked server from Server A to Server B is set up using SQL Native Client 10.0 provider.
On Server B, you have a VIEW that joins couple of tables, TABLE 1 and TABLE2 and a trigger that fires an INSERT into Table 2 when you INSERT into the View.
SERVER B
---------------------
Create 2 tables, 1 view, 1 trigger (instead of)
CREATE TABLE T1 (c1 INT)
GO
CREATE TABLE T2 (c2 INT)
GO
CREATE VIEW vt (cv) AS SELECT c1 FROM t1 UNION ALL SELECT c2 FROM t2
GO
The View confirms to the rules of an Updateable View and a Partitioned View
CREATE VIEW
https://msdn.microsoft.com/en-us/library/ms187956.aspx
Create an INSTEAD OF TRIGGER to INSERT into physical table t2 when insert is fired against the VIEW.
CREATE TRIGGER vt_trig ON vt INSTEAD OF INSERT AS
BEGIN
INSERT INTO t2 (c2) SELECT i.cv FROM INSERTED i
END
SERVER A
---------------------
Create linked server on Server A to Server B with default SNAC provider and call it SNACLinked. Also create another linked server with MSDASQL and ODBC DSN (set up ODBC DSN using SQLODBCsrv driver) and call it MSDASQL_SQL2008.
Set up linked server with MSDASQL and SQLODBC driver instead of SQLNCLI provider:
EXEC sp_addlinkedserver
@server = N'MSDASQL_SQL2008',
@srvproduct = N'',
@provider = N'MSDASQL',
@datasrc = N'sql2008' <- name of odbc system DSN
In this linked server, this will work:
INSERT INTO MSDASQL_SQL2008.INST4.dbo.vt (cv) VALUES (111)
When you INSERT into VIEW on Server A, using linked server created with SNAC, we get the below mentioned error:
INSERT INTO SNACLinked.Test.dbo.vt(cv) values ('16')
ERROR:
OLE DB provider "SQLNCLI10" for linked server "XXXX" returned message "Multiple-step OLE DB operation generated errors. Check each OLE DB status value, if available. No work was done."
Msg 16955, Level 16, State 2, Line 1
Could not create an acceptable cursor.
Locally executing the INSERT on VIEW on Server A works fine.
INSERT into Test.dbo.vt(cv) values (9) – this executes fine.
But when using ODBC DSN for linked server, the INSERT on VIEW works fine from Server A also:
INSERT INTO MSDASQL_SQL2008.Test.dbo.vt(cv) values ('15') - works fine.
The behavior is the same when you execute INSERT statement with OPENQUERY. It fails with the SNAC linked server but works fine with ODBC DSN linked server.
INSERT OPENQUERY (SNACLinked, 'SELECT CV FROM TEST.dbo.vt')
VALUES ('16'); -- fails
INSERT OPENQUERY (MSDASQL_SQL2008, 'SELECT CV FROM TEST.dbo.vt')
VALUES ('16'); -- works fine
It would still fail with the same error when an INDEXED VIEW with a separate TRIGGER is created in place of INSTEAD OF TRIGGER.
CREATE VIEW vt_indexed (cvindexed) with SchemaBinding AS
SELECT c1 FROM dbo.t1 UNION ALL SELECT c2 FROM dbo.t2
GO
CREATE TRIGGER vt_indexed_trig ON vt_indexed INSTEAD OF INSERT AS
BEGIN
INSERT INTO t2 (c2) SELECT i.cvindexed FROM INSERTED i
END
The XACT_ABORT SET option is set to ON for INSERT.
Why INSERT on VIEW with SQLNCLI fails
-------------------------------------------------------
SQL Server wants to do a rowset-based INSERT (cursor based INSERT) operation through the OLE DB API IRowsetChange interface.
SQL Server requests an Updateable rowset for the SELECT statement.
In the SQLNCLI case we are going directly to the OLE DB provider (SQLNCLI or SQLNCLI10).
In the SQLNCLI / OLE DB case we are getting a READ-ONLY cursor that just returns an error about the cursor and does not proceed further. The SQL engine cannot handle this scenario and throws an error.
In the ODBC case, we are really going through MSDASQL/ODBC Driver of 2 layers that interact with SQL Engine. In the ODBC case the cursor gets downgraded to a read-only cursor (you can see the message in in the Profiler, The cursor was not declared). However, MSDASQL has additional logic here to simulate an updateable cursor. It indicates to the SQL Engine that an Updateable rowset is returned. SQL Engine can continue with its logic.
Here is the Error message in Profiler:
Exception Error: 16955, Severity: 16, State: 2 Microsoft SQL Server test 2396 55 2011-02-15 07:35:59.663
User Error Message Could not create an acceptable cursor. Microsoft SQL Server test 2396 55 2011-02-15 07:35:59.663
Exception Error: 16945, Severity: 16, State: 2 Microsoft SQL Server test 2396 55 2011-02-15 07:35:59.663
User Error Message The cursor was not declared. Microsoft SQL Server test 2396 55 2011-02-15 07:35:59.663
Continuing with only the MSDASQL case, SQL Engine does a positioned update through IRowsetChange::InsertRow. MSDASQL generates an INSERT statement as a response to this by parsing the base table/view name, e.g. INSERT INTO Test.dbo.vt( c1) VALUES(?). Since there is a trigger on this read-only VIEW, SQL engine handles it by executing the trigger and everything works.
When opening an updateable rowset over a simple VIEW with a UNION, Rowsetviewer tries 2 things:
declare @p1 int
set @p1=0
declare @p3 int
set @p3=98305
declare @p4 int
set @p4=311300
declare @p5 int
set @p5=0
exec sp_cursoropen @p1 output,N'select * from vt',@p3 output,@p4 output,@p5 output
select @p1, @p3, @p4, @p5
go
declare @p1 int
set @p1=180150009
declare @p3 int
set @p3=8
declare @p4 int
set @p4=1
declare @p5 int
set @p5=1
exec sp_cursoropen @p1 output,N'select * from vt',@p3 output,@p4 output,@p5 output
select @p1, @p3, @p4, @p5
go
Case1, which fails:
scrollopt: 0x18001 = KEYSET_ACCEPTABLE, CHECK_ACCEPTED_TYPES, KEYSET
ccopt: 0x4C004 = OPTIMISTIC_ACCEPTABLE,UPDT_IN_PLACE, CHECK_ACCEPTED_OTPS, OPTIMISTIC
Case2 which succeeds:
scrollopt: 8 = FORWARD_ONLY
ccopt: 1 = READ_ONLY
So in short:
- SQL Server requests an updateable cursor
- For most views, that fails
- MSDASQL is simulating an updateable cursor with insert statements.
So the workaround for SQLNCLI would be to use a stored procedure executed remotely:
CREATE PROCEDURE [dbo].[UpdateViewStoredProc] @Param1 int
AS
BEGIN
SET NOCOUNT ON;
INSERT into Test.dbo.vt(cv) values (@Param1)
END
Execute this stored procedure from remote linked server:
exec SNACLinked.Test.dbo.UpdateViewStoredProc 22
References
----------------
CREATE VIEW
https://msdn.microsoft.com/en-us/library/ms187956.aspx
sp_cursoropen (Transact-SQL)
https://msdn.microsoft.com/en-us/library/ff848737.aspx
Author: Aruna Koppanur (MSFT), SQL Developer Engineer.