Get ROWCOUNT from INSTEAD OF TRIGGER - sql

A legacy app does an INSERT on a table with an instead of trigger and subsequently uses the rowcount for further processing.
We now need to opt out of certain INSERTs with the use of an INSTEAD OF INSERT trigger.
The problem is that ##ROWCOUNT still returns the number of attempted inserts.
For example, a fictitious trigger that will never complete an insert might be
ALTER TRIGGER [dbo].[trig_ACCOUNT_CREDITS_RunningTotalINSERT]
ON [dbo].[ACCOUNT_CREDITS]
INSTEAD OF INSERT
AS
BEGIN
--tried with NOCOUNT ON and OFF
SET NOCOUNT OFF;
--This is an example of the branching logic that might determine
--whether or not to do the INSERT
IF 1=2 --no insert will ever occur (example only)
BEGIN
INSERT INTO dbo.ACCOUNT_CREDITS (COL1, COL2)
SELECT COL1, COL2 from INSERTED
END
END
and some INSERT statements might be
--No rows will be inserted because value of COL1 < 5
INSERT INTO dbo.ACCOUNT_CREDITS (COL1, COL2) VALUES ( 3, 3)
--We would assume row count to be 0, but returns 1
select ##ROWCOUNT
--No rows will be inserted because value of COL1 < 5
INSERT INTO dbo.ACCOUNT_CREDITS (COL1, COL2)
SELECT 1, 1
union all
SELECT 2, 2
--We would assume row count to be 0, but returns 2
select ##ROWCOUNT
I can work around the issue, but it bothers me that I can't trust ##ROWCOUNT. I can find no reference to this issue on SO or those other knowledge banks. Is this simply a case of TRIGGERS ARE EVIL?
Can I affect ##ROWCOUNT?

Some statements may change ##ROWCOUNT inside the trigger.
Statement
SELECT * FROM INSERTED WHERE COL1 < 5
executes and set ##ROWCOUNT to 1
Put statement
SET NOCOUNT ON;
then
IF NOT EXISTS (SELECT * FROM INSERTED WHERE COL1 < 5)
BEGIN
SET NOCOUNT OFF;
INSERT INTO dbo.ACCOUNT_CREDITS (COL1, COL2)
SELECT COL1, COL2 from INSERTED
END

The Problem
I need information in the context of the main process that is only available in the context of the trigger.
The Solution
Whether getting ##ROWCOUNT or anything else from a trigger, or even passing information to a trigger, there are two methods that allow for sharing information with triggers:
SET CONTEXT_INFO / CONTEXT_INFO()
Local Temporary Tables (i.e. tables with names starting with a single #: #tmp)
I posted an example of using CONTEXT_INFO in an answer on a related question over at DBA.StackExchange: Passing info on who deleted record onto a Delete trigger. There was a discussion in the comments on that answer related to possible complications surrounding CONTEXT_INFO, so I posted another answer on that question using a temporary table instead.
Since that example dealt with sending information to a trigger, below is an example of getting information from a trigger as that is what this question is about:
First: Create a simple table
CREATE TABLE dbo.InsteadOfTriggerTest (Col1 INT);
Second: Create the trigger
CREATE TRIGGER dbo.tr_InsteadOfTriggerTest
ON dbo.InsteadOfTriggerTest
INSTEAD OF INSERT
AS
BEGIN
PRINT 'Trigger (starting): ' + CONVERT(NVARCHAR(50), ##ROWCOUNT);
SET NOCOUNT ON; -- do AFTER the PRINT else ##ROWCOUNT will be 0
DECLARE #Rows INT;
INSERT INTO dbo.InsteadOfTriggerTest (Col1)
SELECT TOP (5) ins.Col1
FROM inserted ins;
SET #Rows = ##ROWCOUNT;
PRINT 'Trigger (after INSERT): ' + CONVERT(NVARCHAR(50), #Rows);
-- make sure temp table exists; no error if table is missing
IF (OBJECT_ID('tempdb..#TriggerRows') IS NOT NULL)
BEGIN
INSERT INTO #TriggerRows (RowsAffected)
VALUES (#Rows);
END;
END;
Third: Do the test
SET NOCOUNT ON;
IF (OBJECT_ID('tempdb..#TriggerRows') IS NOT NULL)
BEGIN
DROP TABLE #TriggerRows;
END;
CREATE TABLE #TriggerRows (RowsAffected INT);
INSERT INTO dbo.InsteadOfTriggerTest (Col1)
SELECT so.[object_id]
FROM [master].[sys].[objects] so;
PRINT 'Final ##ROWCOUNT (what we do NOT want): ' + CONVERT(NVARCHAR(50), ##ROWCOUNT);
SELECT * FROM #TriggerRows;
Output (in Messages tab):
Trigger (starting): 91
Trigger (after INSERT): 5
Final ##ROWCOUNT (what we do NOT want): 91
Results:
RowsAffected
5

Related

How can I use the OUTPUT from a stored procedure within an INSERT statement?

I'm trying to copy data from one table to another and add one column to the target table that isn't present in the source table using INSERT INTO and a stored procedure to generate the value for the additional column.
The target table has counter field (integer) that needs to be incremented for every row inserted (it is not an IDENTITY column, incrementing this counter is handled by other code).
The Counter table has counters for multiple tables and need to pass the Counter Name as a parameter to the stored procedure and use output as the additional column for the target table.
I have a stored procedure and can pass it a parameter (counter name, in example below counter name is "Counter_123") and it has as output value as the new counter value.
I can run the stored procedure like this and it works fine:
declare #value int
exec GetCounter 'Counter_123', #value output
PRINT #value
In this case if the counter was 3, the output is 4.
Here is what I'm trying to do in my INSERT INTO:
INSERT INTO Target_Table (col1, col2, col3, uspGetCounter 'Counter_123')
SELECT (val1, val2, val3)
FROM Source_Table
WHERE ...
Error returned is syntax error near 'Counter_123'
I've also tried to put the stored procedure in the values list but that doesn't work either.
What is the syntax to call a stored procedure, pass it a parameter, and use the output in the INSERT INTO query as a value for a column?
Here is the stored procedure code for reference:
CREATE PROCEDURE [dbo].[uspGetCounter]
#CounterName varchar(30) = '',
#LastValue int OUTPUT
AS
BEGIN
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
if not exists (select * from Counter where COUNTER_NAME=#CounterName)
insert Counter (COUNTER_NAME, LAST_VALUE)
values (#CounterName,0)
select #LastValue=LAST_VALUE+1
from Counter (holdlock)
where COUNTER_NAME= #CounterName
update Counter
set LAST_VALUE=#LastValue,
LAST_UPDATED=getdate(),
from Counter
where COUNTER_NAME=#CounterName
COMMIT TRANSACTION
END
Assuming you really need to roll your own sequence generator, because using a built in sequence is going to be much easier, then with your current logic you can only insert a single row at a time, because your stored procedure provides no mechanism to allocate a block of values in one go. You would therefore need a loop of some sort, maybe a cursor e.g.
DECLARE #Counter int, #Val1 int, #Val2 int, #Val3 int;
DECLARE MyCursor CURSOR LOCAL FOR
SELECT val1, val2, val3
FROM Source_Table
WHERE ...;
WHILE 1 =1 BEGIN
FETCH NEXT FROM MyCursor INTO #Val1, #Val2, #Val3;
IF ##FETCH_STATUS != 0 BREAK;
EXEC uspGetCounter 'Counter_123', #Counter OUTPUT;
INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
VALUES (#Val1, #Val2, #Val3, #Counter);
END;
Where you run your SP for each row you need to insert and obtain a new sequence value.
If using this approach I would also modify your SP as follows for performance:
-- Attempt an update and assignment in a single statement
UPDATE dbo.[Counter] SET
#LastValue = LAST_VALUE = LAST_VALUE + 1
, LAST_UPDATED = GETDATE()
WHERE COUNTER_NAME = #CounterName;
-- If doesn't exist (less likely case as only the first time the counter is accessed)
IF ##ROWCOUNT = 0 BEGIN
SET #LastValue = 1;
-- Create a new record if none exists
INSERT INTO dbo.[Counter] (COUNTER_NAME, LAST_VALUE, LAST_UPDATED)
SELECT #CounterName, #LastValue, GETDATE();
END;
Which reduces access and speeds up the SP.
However, again assuming you need to roll your own sequence, if you are using this to copy data frequently you would be much better off modifying your stored procedure to allocate a block of values in one go i.e. when you call your SP you pass a parameter #NumValuesRequired and then your return value is the first of a block that long. Then you can carry out your copy in a single go e.g.
DECLARE #RecordCount int, #CounterStart int;
SELECT #RecordCount = COUNT(*)
FROM Source_Table
WHERE ...;
EXEC uspGetCounter2 'Counter_123', #RecordCount, #CounterStart OUTPUT;
INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
SELECT val1, val2, val3, #CounterStart + ROW_NUMBER() OVER () - 1
FROM Source_Table
WHERE ...;
First of all, the the procedure is in the wrong place. That part of the INSERT statement expects column names, not column values. Anything you do with the procedure would go in the SELECT clause for the statement, if you could run it that way (which you can't).
That out of the way, the existing Counter table and stored procedure are obsolete. They exist to solve a problem that has since been rolled into SQL Server as a core feature. Therefore the BEST solution here is converting the counter (all of them) to use a SEQUENCE and NEXT VALUE FOR syntax.
Then, assuming you have primed the sequence to the correct value the INSERT would look like this:
INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT (val1, val2, val3, NEXT VALUE FOR Counter_123)
FROM Source_Table
WHERE ...
If for some reason that's not an option, I would roll the counter logic into the query to run in a single transaction, rather many many transactions, like this:
BEGIN TRANSACTION
DECLARE #LastValue int;
SELECT #LastValue = LAST_VALUE
FROM Counter (holdlock)
WHERE COUNTER_NAME = 'Counter_123'
INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT val1, val2, val3, #LastValue + ROW_NUMBER() OVER (ORDER BY val1, val2, val3)
FROM Source_Table
WHERE ...
UPDATE Counter
SET LAST_VALUE = (SELECT MAX([Counter_123]) FROM [Target_Table])
,LAST_UPDATED=current_timestamp
WHERE COUNTER_NAME = 'Counter_123'
COMMIT

Create a string from inserted.id via trigger

I'm creating a trigger on insert that will create a new record in another table using the id from the newly inserted row.
I need to format the id before I insert it into the other table. This is my code so far.... having problems creating the formated #PseudoID.
CREATE TRIGGER OnInsertCreateUnallocatedPseudo
ON tblTeams
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for trigger here
DECLARE #PseudoID NVARCHAR(50), #tmID NVARCHAR(10)
SELECT #tmID = CONVERT(nvarchar(10),inserted.tmID) FROM inserted
--NEED SOME CODE TO CREATE A PADDED OUT PseudoID e.g.
-- if #tmID = '7' then #PseudoID = 'PSU0007'
-- if #tnID = '25' then #PseudoID = 'PSU0025'
INSERT INTO [dbo].[tblUsersPseudo].....
END
You can't assign it to a variable within the trigger since there could be multiple rows, but you could do something like this to insert into the other table.
LEFT('PSU0000',LEN('PSU0000')-LEN(CONVERT(nvarchar(10),inserted.tmID))) + CONVERT(nvarchar(10),inserted.tmID)
You can't assume that Inserted has only a single row, you have to treat it as a table with 0-N rows and carry out set-based operations on it (rather than procedural).
FORMAT will format your existing id into the new format you require.
INSERT INTO [dbo].[tblUsersPseudo] (id, col1, col2, ...)
SELECT FORMAT(id,'PSU0000')
, col1, col2
FROM Inserted;

After insert not working

all id columns has auto_increment
In my trigger:
ALTER trigger [dbo].[mytrig]
on [dbo].[requests]
after INSERT, UPDATE
as
begin
declare #MyId1 int
set #MyId1 = (select Id from inserted)
declare #MyId2 int
declare #MyId3 int
if (select column1 from inserted) = 1
begin
insert into [dbo].[contracts] select column1,column2,column3 .... from inserted
set #MyId2 = SCOPE_IDENTITY()
insert into [dbo].[History] select column1,column2,column3 .... from inserted
set #MyId3 = SCOPE_IDENTITY()
insert into [dbo].[contracts_depts](Id_Contract ,column5) select #MyId2,column6 from request_depts where Id_request=#MyId1
insert into [dbo].[History_depts] (Id_InHistory,column5) select #MyId3,column6 from request_depts where Id_request=#MyId1
end
end
#MyId1 returns value only after update but not after insert. Do I have to use scope_identity() or something ?
Your main issue is: you're assuming the triggers is called once per row - that is NOT the case!
The trigger is called once per statement, and if your statement affects multiple rows, the Inserted pseudo table will contain multiple rows - so your statement here
set #MyId1 = (select Id from inserted)
really isn't going to work - it will select one arbitrary row (out of however many there are).
You'll need to rewrite your trigger to take this fact into account! Assume that Inserted contains 100 rows - how do you want to deal with that? What are you trying to achieve? Triggers don't return values - they will record into an audit table, or update other rows, or something like that ....

Count Rows Error in Trigger

I wrote this trigger below. it is generating Error at COUNT(*) From. I want that when any row inserts into table 'Users' with this trigger currently present folders in Assignments should assign to User.
For Example: I add new row to table Users suppose userD. Then with the help of this trigger Present folders like folderA, folderB, folderC should assign to userD with folder right Visible by default. I have written this Trigger below but it is givig Error at Count(*) From
CREATE TRIGGER Trigger_Insert ON Users
FOR INSERT
AS
declare #userid int;
declare #username nvarchar(50);
declare #useremail nvarchar(50);
declare #userpassword nvarchar(50);
select #userid=i.user_id from inserted i;
select #username=i.user_name from inserted i;
select #useremail=i.user_email from inserted i;
select #userpassword=i.user_password from inserted i;
DECLARE #intFlag INT
SET #intFlag =1
WHILE (#intFlag <=COUNT(*) FROM Assignments;) // Error Here
BEGIN
insert into UAssignment_Rights(
ua_rights_id,ua_rights_name,assignment_id,user_id)
values(#userid,'Visible','','');
SET #intFlag = #intFlag + 1
PRINT 'AFTER INSERT trigger fired.'
END
GO
Can you please help me to solve this issue.
Answer updated to comply with info from comments.
My point is that most of the times you do not need loops or cursor in Sql Server. Usually set based approach is simpler and faster. In this case you might use form of insert that does not insert fixed values but rather result of select.
CREATE TRIGGER Trigger_Insert ON Users
FOR INSERT
AS
-- this is mandatory in trigger - you do not want ##rowcount reported to applications
-- to change as a result of statement in trigger (all of them, including set and select)
set nocount on
-- You can simply take assignment_id directly from Assignments table
insert into UAssignment_Rights(ua_rights_name, assignment_id, user_id)
select 'Visible', a.assignment_id, i.user_id
from Inserted i
cross join dbo.Assignments a
PRINT 'AFTER INSERT trigger fired.'
GO
P.S. cartesian product is result of join without join condition. Meaning that returned rows are all possible cobinations of each row from left table with each row from right table.
COUNT(*) should only be used as part of a SELECT or HAVING statement, see MSDN for more information on aggregate functions.
If Assignments will not change as part of the WHILE loop, try replacing...
WHILE (#intFlag <=COUNT(*) FROM Assignments;) // Error Here
With...
DECLARE #AssignmentsCount INT
SELECT #AssignmentsCount = COUNT(*) FROM Assignments
WHILE #intFlag <= #AssignmentsCount
This means that the COUNT(*) is only done once.
However, if the number of Assignments could change during the loop, the replace it with...
WHILE #intFlag <= (SELECT COUNT(*) FROM Assignments)

Can a storeProcecdure insert fire Trigger?

SQL Server 2008
I have trigger defined on
TABLE_A for 'INSTEAD OF INSERT' and TABLE_B for 'INSTEAD OF INSERT'.
Both the triggers perform merge with the inserted table.
TABLE_A insert is done by user/code and is working well, trigger for insert is fired.
I have Stored procedure SP_1 inside TABLE_A TRIGGER.
SP_1 Inserts data from TABLE_A into TABLE_B based on some conditions.
But the problem is when the stored procedure (SP_1) is inserting data, the trigger on TABLE_B is not fired and the data is just inserted as it is.
So can stored procedure inserts fire triggers?
Pseudo-code
ALTER TRIGGER [dbo].[trgtable_AInsert] ON [dbo].[TABLE_A]
Instead of INSERT
AS
BEGIN
SET NOCOUNT ON;
IF exists(SELECT * FROM INSERTED)
BEGIN
MERGE
.......
...........
..............
end
EXEC SP_1 #employee_id
end
ALTER TRIGGER [dbo].[trgtableB_Insert] ON [dbo].[TABLE_B]
Instead of INSERT
AS
BEGIN
SET NOCOUNT ON;
IF exists(SELECT * FROM INSERTED)
BEGIN
MERGE
.......
...........
..............
end
end
ALTER PROCEDURE [dbo].[SP_1] #employeeid int
AS
BEGIN
BEGIN TRANSACTION
insert into TABLE_B
.......
...........
..............
from TABLE_A
where employee_ID is #employeeid
COMMIT TRANSACTION
END
Yes triggers can fired by stored procedure inserts!
But I think the problem is that you should try to use AFTER instead of INSTEAD OF triggers in this case. Becasue I can't see all of your code, but it is possible, that the insert is not done because you overrided it in the Instead Of triggers. With AFTER triggers you should have no problems with firing the second trigger.
This is too big for a comment, and needs formatting, so posting as an "answer".
Yes, triggers will fire in this case. Taking your example and slightly modifying it (note the warnings though):
create table Table_A (ID int not null)
go
create table Table_B (ID int not null)
GO
CREATE PROCEDURE [dbo].[SP_1] #employeeid int
AS
BEGIN
BEGIN TRANSACTION
insert into TABLE_B (ID)
SELECT ID from TABLE_A
where ID = #employeeid
COMMIT TRANSACTION
END
GO
Creating the triggers:
CREATE TRIGGER [dbo].[trgtable_AInsert] ON [dbo].[TABLE_A]
Instead of INSERT
AS
BEGIN
SET NOCOUNT ON;
IF exists(SELECT * FROM INSERTED)
BEGIN
MERGE
into Table_A a
using inserted i on a.id = i.id
when not matched then insert (ID) values (i.id);
end
--Wrong code, just for example
declare #employee_id int
select #employee_id = ID from inserted --BAD CODE, Ignores multiple rows
EXEC SP_1 #employee_id
end
GO
CREATE TRIGGER [dbo].[trgtableB_Insert] ON [dbo].[TABLE_B]
Instead of INSERT
AS
BEGIN
SET NOCOUNT ON;
IF exists(SELECT * FROM INSERTED)
BEGIN
MERGE
into Table_B b
using inserted i on b.id = i.id
when not matched then insert (ID) values (i.id+5);
end
end
GO
And executing a trial insert into Table_A:
insert into Table_A (ID) values (1),(2)
select * from Table_B
On my machine, at the present time, I get a final result of a single row with the value "7". Others may run this sample and get the result "6", because triggers only run once per statement, rather than once per row. But as you can see, both triggers have fired.
As i previously mentioned in the comments #András Ottó
Merge
using(... = "column with possible null values" AND
... = ... AND
... = ...
)
of merge was not working correctly and the records were always inserted.
1 = 1 and E=E and NULL=NULL is not true. (of-course sql 101)
I have overlooked this column and did not place where clause properly to get rid of null values so ended up inserting all the time. Fixing that everything ended up working.
Thanks for the help Every1. Cheers
Apologies.
I'm not going to mark it answered because it is purely my mistake which was not fully mentioned in the question.