Bluedog67

Random Thoughts About SQL Server and My Dog Blue

About Bluedog67

My name is Stephen Horne. I am a well-trained owner of an Australian Cattle Dog named Blue. I also develop software mainly using SQL Server, ASP.NET, and C#. I love data, databases, T-SQL, and turning raw data into actionable information. My plan for this blog is to write about SQL Server and sometimes my dog Blue. Please contact me at stephen at bluedog67 dot com. Follow me on Twitter at bluedog67.

Strange INSTEAD OF Trigger Side Affect

In previous post here I described a seemingly strange side affect of an INSTEAD OF trigger. Within the INSTEAD OF trigger I didn't update any records - I just returned an error. No rows are actually being affected. However, SSMS (SQL Server Management Studio) shows records *are* being affected. The count is equal to the count that would have been affected had the INSTEAD OF trigger not existed or if it had been disabled.

I wanted to see if this was just SSMS behavior. I created a simple C# Console Application project in Visual Studio 2008 against a SQL Server 2008 Dev Ed database.

Following is database setup code.

CREATE DATABASE MyDB;
GO

USE MyDB
GO

CREATE TABLE dbo.MyTable
(
ID int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
MyColumn varchar(50) NOT NULL
);
GO

-- sample data
INSERT INTO dbo.MyTable (MyColumn) VALUES ('One');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Two');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Three');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Four');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Five');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Six');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Seven');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Eight');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Nine');
INSERT INTO dbo.MyTable (MyColumn) VALUES ('Ten');
GO

CREATE TRIGGER dbo.tr_MyTable_StopDataMods
ON dbo.MyTable
INSTEAD OF INSERT, DELETE, UPDATE
AS
BEGIN
  
  SET NOCOUNT ON;
  
  RAISERROR(
  N'The data in table %s is static. All data modification operations are blocked. Disable the trigger %s to modify table data.', -- Message text.
  16, -- Severity
  1, -- State,
  'dbo.MyTable', -- first argument (table name)
  'dbo.tr_MyTable_StopDataMods' -- second argument (trigger name)
  );
  
END
GO

Test with the following code.

UPDATE MyTable SET MyColumn = 'X';

Should see expected error message and the "10 row(s) affected" message. If you do a SELECT * FROM MyTable; you should see that the data was not modified.

Now let's switch gears from T-SQL over to C# and .NET. Following is complete code for the application. You may have to modify the Connection String as required. The database UPDATE operation is the same as above that you executed in SSMS that resulted in a error, no rows being updated, but SSMS showing "10 row(s) affected."

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      string strConnection = "Data Source=localhost;Initial Catalog=MyDB;Integrated Security=True";
      using (SqlConnection cn = new SqlConnection(strConnection))
      {
        cn.Open();

        using (SqlCommand cmd = new SqlCommand())
        {
          cmd.Connection = cn;
          cmd.CommandType = CommandType.Text;
          cmd.CommandText = "UPDATE MyTable SET MyColumn = 'X';";
          int RowsAffected = cmd.ExecuteNonQuery();
          Console.WriteLine("Rows Affected: " + RowsAffected.ToString());
        }
      }

      string x = Console.ReadLine(); // Keep Console from closing
    }
  }
}

Run the application in debug mode. Should error out on the cmd.ExecuteNonQuery command with an unhandled SqlException with the expected error message: "The data in table dbo.MyTable is static. All data modification operations are blocked. Disable the trigger dbo.tr_MyTable_StopDataMods to modify table data." See the screenshot below. In the locals window take a look at cmd.Non-Public members._rowsAffected. Shows a value of 10 (ten). Now isn't that interesting. This side affect associated with the INSTEAD OF trigger that updates zero rows is also seen when making calls through other APIs. To confirm this behavior you could possibly do additional testing using other INSERT, UPDATE, and DELETE commands.



Any feedback or ideas that you have will be appreciated. Thanks!

Update:

I modified the INSTEAD OF trigger and removed the RAISERROR command. Now the INSTEAD OF trigger just has the SET NOCOUNT ON command.

ALTER TRIGGER dbo.tr_MyTable_StopDataMods
ON dbo.MyTable
INSTEAD OF INSERT, DELETE, UPDATE
AS
BEGIN

SET NOCOUNT ON;

END
GO

When you rerun the C# Console app, it runs successfully without error and shows the following.



If you again look at the underlying data in the table - it is unaffected by the UPDATE statement. No rows were actually updated.

To further confirm disable the trigger and rerun the C# Console app. It should again show Rows Affected: 10. This time when you query the data in the table - the data will have been updated.

According to the MSDN documentation located here, the System.Data.SqlClient.SqlCommand.ExecuteNonQuery() method "Executes a Transact-SQL statement against the connection and returns the number of rows affected." The documentation also states "For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command. When a trigger exists on a table being inserted or updated, the return value includes the number of rows affected by both the insert or update operation and the number of rows affected by the trigger or triggers. For all other types of statements, the return value is -1. If a rollback occurs, the return value is also -1." The documentation is not very helpful.

It is clear that when INSERT, UPDATE, or DELETE statements are executed against a table or view with an INSTEAD OF trigger, regardless of what code is actually executed within the INSTEAD OF trigger, the following is occuring: A SELECT statement is being run against the table or view to determine the number of rows that would have been affected had the trigger not existed. This value is being passed back to the process/application requesting the INSERT, UPDATE, or DELETE operation as the count of rows being affected even though the actual number of rows affected could be zero, the same, or something completely different.

Again, thanks for reading and any feedback is appreciated. Thanks!

Categories: SQL Server
Permalink | Comments (7) | Post RSSRSS comment feed

Comments

Walden Leverich United States

Friday, January 08, 2010 12:48 AM

Walden Leverich

I'm not sure that's incorrect behaviour. The message doesn't say 10 records "updated" it says "affected" and in the case of an instead of trigger who is to say what the intended effect was?

Stephen Horne (bluedog67) United States

Friday, January 08, 2010 2:27 AM

Stephen Horne (bluedog67)

Thanks for feedback Walden. I believe you are correct. I don't think this is incorrect behavior either - just strange. INSTEAD OF triggers are strange in and of themselves. Not suprised that any associated behavior is strange also.

Arnie Rowland United States

Friday, January 08, 2010 1:02 PM

Arnie Rowland

Not 'strange' or incorrect.

This is perfectly normal and expected behavior.

The INSTEAD OF TRIGGER fired and evaluated 10 rows for potential trigger action.
So 10 rows were affected. However in this case, nothing happened.

stephen United States

Friday, January 08, 2010 3:09 PM

stephen

Arnie: Thanks for your feedback. After both studying and thinking more about INSTEAD OF triggers I believe I understand why they behave the way they do. The behavior is clearly not incorrect. Maybe instead of (heh) saying that the behavior is strange - I will say that the behavior could either confuse or give a false impression to a user/developer requesting INPUT UPDATE or DELETE operations that rows were actually affected when none were. In SSMS I often rely on the (X row(s) affected) message and in ADO.NET the SqlCommand.ExecuteNonQuery() return to ensure that rows were updated plus the correct number of rows were updated. I think that there may be value in exposing within the body of an INSTEAD OF trigger this rows affected "variable" so that it can be set programmatically. The default could be the current value but the developer could replace the value with zero, -1, or a different value if desired. Thoughts? Good idea or bad?

Again, thanks for your feedback and ideas. Thanks for reading! Stephen Horne (bluedog67)

Arnie Rowland United States

Friday, January 08, 2010 5:49 PM

Arnie Rowland

It is normal to use the SET NOCOUNT ON statement in stored procedures. Rarely is that useful information to the applications, and often it represents a second unexpected resultset to the application -and that can be confusing...

It is a good practice to use a OUTPUT parameter, and have the stored procedure determine what kind of completion messaging is returned to the client application.

In the following code example, two 'n rows affected' statements are returned, one for the INSERT, and another for the TRIGGER. How would the application handle that confusing message?

USE tempdb;
GO

CREATE TABLE TestTable
   (  RowId    int IDENTITY,
      MyValue  varchar(20),
      TestFlag bit DEFAULT 0
   );
GO
  
CREATE TRIGGER tr_TestTableInsert
   ON TestTable
   FOR INSERT
AS
   IF @@ROWCOUNT = 0
      RETURN
   UPDATE t
      SET t.TestFlag = 1
      FROM TestTable t
         JOIN inserted i
         ON t.RowID = i.RowID
      WHERE i.MyValue LIKE 'Test%';
GO

INSERT INTO TestTable
   ( MyValue )
   VALUES
      ( 'TestData1' ),
      ( 'TestData2' ),
      ( 'MyData1' );
      
SELECT *
FROM TestTable;

DROP TABLE TestTable;

Stephen Horne (bluedog67) United States

Thursday, January 14, 2010 7:47 PM

Stephen Horne (bluedog67)

Arnie: Thanks for the comment. Sorry for the delay in getting back. I ran your code (temporarily commenting out the INSERT and DROP TABLE statements). I modified the C# Console application code listed below. When you run this code using a matching INSERT statement you receive a Rows Affected value of 5 (the sum of the two rows affected values - 3 for insert and 2 inside of trigger). If you modify your trigger to include a SET NOTCOUNT ON statement the C# code returns 3. It appears that the SET NOCOUNT ON command affects the AFTER triggers but not the INSTEAD OF trigger. But I could be wrong in my analysis. Again thank you for your interest and ideas. Stephen

C# Console Application Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      string strConnection = "Data Source=localhost;Initial Catalog=tempdb;Integrated Security=True";
      using (SqlConnection cn = new SqlConnection(strConnection))
      {
        cn.Open();

        using (SqlCommand cmd = new SqlCommand())
        {
          cmd.Connection = cn;
          cmd.CommandType = CommandType.Text;
          cmd.CommandText = "INSERT INTO TestTable ( MyValue ) VALUES ( 'TestData1' ), ( 'TestData2' ), ( 'MyData1' ); ";
          int RowsAffected = cmd.ExecuteNonQuery();
          Console.WriteLine("Rows Affected: " + RowsAffected.ToString());
        }
      }

      string x = Console.ReadLine(); // Keep Console from closing
    }
  }
}

Arnie Rowland United States

Friday, January 15, 2010 2:04 AM

Arnie Rowland

The SET NOCOUNT ON statement equally affects FOR and INSTEAD OF triggers. It suppresses the rowcount return value for the scope of the statement. If the statement exists in the TRIGGER, then it only affects the TRIGGER.

You are correct that your modification returns a rowcount value. However, I would not put the SET NOCOUNT ON statement in the TRIGGER, I would add it to the stored procedure, or just add to the commandtext.

cmd.CommandText = "SET NOCOUNT ON; INSERT INTO TestTable ( MyValue ) VALUES ( 'TestData1' ), ( 'TestData2' ), ( 'MyData1' ); ";

You really don't want the application to have to deal with the rowcount affected information. If your application requires a return count, then have the stored procedure determine the correct value and return it as an OUTPUT parameter.