how to debug a recursive trigger - sql

I have a recursive trigger, that seems to do exactly what I want it to with no recursion, however when I turn recursion on I get the error: "Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32)"
This should not happen, as I expect 2 or maybe 3 levels of nesting, so I need to debug the trigger and work out what exactly is going on. I added a print statement, but that does not work...
How do you go about debugging a recursive trigger?
ALTER TRIGGER [dbo].[DataInstance2_Trigger]
ON [dbo].[DataInstance]
AFTER UPDATE
AS
BEGIN
UPDATE DataInstance
SET
DataInstance.IsCurrent = i.IsCurrent
FROM DataInstance di
Inner join DataContainer dc on
di.DataContainerId = dc.DataContainerId
Inner join Inserted i on
dc.ParentDataContainerId = i.DataContainerId
WHERE di.IsCurrent != i.IsCurrent
declare #x int
SET #X = (select max(DataContainerId) from Inserted)
print #X
END

You can include a SELECT statement in your trigger (I've just tried this - try SELECT * FROM DataInstance prior to the UPDATE in the trigger).
I've repro'ed the problem - the UPDATE in the trigger is causing the trigger to fire again even if there are no rows updated. A suitable fix would be to wrap the UPDATE statement within the trigger in an IF (SELECT COUNT(*) FROM INSERTED) <> 0 BEGIN ... END block.

Related

Trigger running every time despite conditional statement

I am trying to write an update trigger on a table which would cause it to run an additional update statement only if a certain column has been changed, so far the trigger runs the update no matter what, hoping maybe someone can see what I am doing wrong here.
Here is the trigger.
ALTER TRIGGER [dbo].[StatusChangedUpdateTrigger]
ON [dbo].[Trans_Order]
AFTER UPDATE
AS
DECLARE #OldOrderStatusId INT, #NewStatusOrderId INT, #ERRNUM INT;
BEGIN
SET #OldOrderStatusId = (SELECT OrderStatusId FROM deleted);
SET #NewStatusOrderId = (SELECT OrderStatusId FROM inserted);
IF (#OldOrderStatusId != #NewStatusOrderId)
SET NOCOUNT ON;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted)
END
For some reason this is running no matter what, I can never set StatusChanged to 0 as it will automatically flip it back to 1 even if the OrderStatusId hasn't changed. So my update statement is running no matter what, so I am guessing I am doing something wrong in the if statement.
Hmmmm . . . Your logic seems strange. I would expect:
UPDATE t
SET StatusChanged = 1
FROM Trans_Order t JOIN
Inserted i
ON t.id = i.id JOIN
Deleted d
ON t.id = d.id
WHERE i.OrderStatusId <> d.OrderStatusId;
You might need to take NULL values into account -- although your code does not.
Note that your code is just a bug waiting to happen, because it assumes that inserted and deleted have only one row.
The specific problem with your code is that it is really:
IF (#OldOrderStatusId != #NewStatusOrderId)
BEGIN
SET NOCOUNT ON;
END;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted);
Your indentation has confused the logic. However, you should still use the set-based version so the trigger does not fail.
The correct way to approach your trigger is as follows:
create or alter trigger [dbo].[StatusChangedUpdateTrigger] on [dbo].[Trans_Order]
after update
as
set nocount on
if ##RowCount=0 return
if Update(OrderStatusId)
begin
update t
set statusChanged=1
from inserted i join deleted d on d.id=i.id and d.OrderStatusId != i.OrderStatusId
join Trans_Order t on t.id=i.id
end
Always test ##rowcount and return if no rows updated.
Always put set options before DML
As you are only looking to update if a specific column is updated you can test specifically for that and if the update statement that's run doesn't touch that column the trigger will not run.
This will correctly account for multiple rows being updated and only update those where the new value is different to the old value.

SQL - Using a Trigger to occasionally allow row deletions

I have some views I'm working with, and I need to set up a trigger to prevent the deletion of any data within the view if the data table is locked by a variable in another table. I already have the trigger set up, and have no issue with setting up the replacement of the delete command using an 'INSTEAD OF DELETE' trigger, but my issue now is how to preserve the incoming delete query for the times where deletions are allowed. In this case, I'm using SQL Server.
For example, if the control table says my view is locked, then any delete query sent to the database should be stopped, and an error message should be returned (no problems here, have this already). However, if the control table says my view is unlocked, I need to be able to allow the delete query to delete data from the view (here I need help).
Code:
CREATE TRIGGER [database_name].[trigger_name]
ON [database_name].[view_name]
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON
IF NOT EXISTS (SELECT * FROM [database_name].[control_table_name]
WHERE Control_Item = 'view_name' AND Can_modify = 'N')
BEGIN
SET NOCOUNT OFF
--NEW DELETION STATEMENT GOES HERE, NEEDS TO BE DUPLICATE OF REPLACED DELETE QUERY--
END
ELSE
BEGIN
RAISEERROR('Deletions are currently disabled!', 11, 1);
END
END
I'm sure there's some stupid logic statement or something I'm missing but I'll appreciate all the help I can get. Thanks!
You cannot delete from the view directly - you need to delete from the underlying table(s).
So basically, you need to add one (or several) DELETE statements for your tables - and hopefully, you've included the primary keys from all the underlying tables into your view!
CREATE TRIGGER [database_name].[trigger_name]
ON [database_name].[view_name]
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON
IF NOT EXISTS (SELECT * FROM [database_name].[control_table_name]
WHERE Control_Item = 'view_name' AND Can_modify = 'N')
BEGIN
SET NOCOUNT OFF
DELETE FROM dbo.YourFirstTable
WHERE PrimaryKeyColumn IN (SELECT FirstPrimaryKeyColumn FROM Deleted);
-- if the view is built on top of several tables, you possibly need multiple DELETE statements here
DELETE FROM dbo.YourSecondTable
WHERE PrimaryKeyColumn IN (SELECT SecondPrimaryKeyColumn FROM Deleted);
END
ELSE
BEGIN
RAISEERROR('Deletions are currently disabled!', 11, 1);
END
END
Perhaps a 'FOR DELETE' can do the trick, and do a rollback when the delete is not allowed.
CREATE TRIGGER [database_name].[trigger_name]
ON [database_name].[view_name]
FOR DELETE
AS
SET NOCOUNT ON
IF EXISTS (SELECT *
FROM [database_name].[control_table_name]
WHERE Control_Item = 'view_name' AND Can_modify = 'N')
BEGIN
RAISEERROR('Deletions are currently disabled!', 11, 1) WITH NOWAIT
ROLLBACK
END
SET NOCOUNT OFF
GO

How to delete all rows without deleting the last returned row in SQL Server?

I want to delete all the rows from a SELECT without deleting the last returned row by using a trigger when a delete query is executed.
This trigger doesn't work so any help is greatly appreciated.
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS
DECLARE #nr INTEGER;
IF (EXISTS(SELECT * FROM DELETED))
BEGIN
SET #nr = (SELECT COUNT(*) FROM DELETED);
DELETE FROM (
SELECT TOP(#nr - 1)* FROM OferteSpeciale
INNER JOIN DELETED ON OferteSpeciale.codP = Deleted.codP
AND OferteSpeciale.codM = Deleted.codM
AND OferteSpeciale.dela = Deleted.dela)
END
Here is an example of getting your concept to work properly:
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS BEGIN
DECLARE #nr INT
SET #nr = (SELECT COUNT(*) FROM DELETED)
IF (#nr > 1) BEGIN
DELETE o
FROM OferteSpeciale AS o
INNER JOIN (SELECT TOP (#nr - 1) * FROM DELETED /* ORDER BY ??? */) AS d
ON o.codP = d.codP
AND o.codM = d.codM
AND o.dela = d.dela
END
END
Note the syntax for a delete with a join. Also note that we're arbitrarily choosing the 1 row to keep. I would suggest, as #RBarryYoung has mentioned, specifically ordering the set by something to know which row we are keeping.
Another way of doing this which could avoid the somewhat dynamic TOP clause (clever, BTW) would be to specifically exclude the record you want to keep using NOT EXISTS/IN
Also, you probably want to avoid trigger recursion and nested triggers in this case.

SQL Server trigger won't run the complete update statement?

Problem: I wrote a trigger that is supposed to update the INVOICE table after an INSERT into the LINE table but it won't update the invoice table. The trigger fires but the Update statement block won't execute.
I debugged while I executed the INSERT into the line table and found out as soon as the it gets to the UPDATE statement it jumps over it and doesn't run that code. I have been trying to figure this out for a couple of days now.
Here is my trigger code
ALTER TRIGGER [dbo].[trgInvoiceInsert] ON [dbo].[LINE]
AFTER INSERT
AS
declare #linUnits numeric(9,2);
declare #linPrice numeric(9,2);
declare #invNum int;
select #linUnits = LINE_UNITS from inserted;
select #linPrice = LINE_PRICE from inserted;
select #invNum = INV_NUMBER from inserted;
BEGIN
UPDATE i --From here it jumps to the start of the next Update
SET INV_SUBTOTAL = INV_SUBTOTAL + (#linUnits * #linPrice)
FROM dbo.INVOICE as i
WHERE i.INV_NUMBER = #invNum
UPDATE i
SET INV_TOTAL = INV_SUBTOTAL + INV_TAX
FROM dbo.INVOICE as i
WHERE i.INV_NUMBER = #invNum
PRINT 'Trigger fired Successfully.'
END
Well, using a statement like this:
select #linUnits = LINE_UNITS from inserted;
indicates that you're assuming that the trigger fires per row - it does not.
SQL Server triggers are fired once per statement - so if your statement inserts multiple rows into the Line table, your trigger will fire once for the complete statement, but the Inserted pseudo table will contain multiple rows of data.
So you need to rewrite your trigger to take that into account - the Inserted table can (and will!) contain multiple rows - and your trigger code needs to deal with that.

how to create before update trigger in sql server 2005

Is there anyway where I can create a trigger which will execute before the update/delete takes place( and then the actual update/delete takes place)? and how can I drop a trigger from a table?
to drop a trigger use:
--SQL Server 2005+, drop the trigger, no error message if it does not exist yet
BEGIN TRY DROP TRIGGER dbo.TrigerYourTable END TRY BEGIN CATCH END CATCH
GO
--drop trigger pre-SQl Server 2005, no error message if it does not exist yet
if exists (select * from sysobjects where id = object_id(N'[dbo].[TrigerYourTable ]') and OBJECTPROPERTY(id, N'IsTrigger') = 1)
drop trigger [dbo].[TrigerYourTable ]
GO
OP said in a comment:
...suppose I have to check childcount of
a perticular user if that is more than
5 do not update the user.how can I do
that using instead of trigger?
You don't really need to prevent the original update, you can let it happen, and then in the trigger check for the problem and rollback if necessary. This is how to enforce the logic for one or many affected rows, when you need to JOIN to determine the childcount of the affected rows:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
AFTER UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT
1
FROM INSERTED i
INNER JOIN YourChildrenTable c ON i.ParentID=c.ParentID
GROUP BY i.ParentID
HAVING COUNT(i.ParentID)>5
)
BEGIN
RAISERROR('Count of children can not exceed 5',16,1)
ROLLBACK
RETURN
END
GO
It will throw the error if there is a violation of the logic, and the original command will be subject to a rollback.
If childcount is a column within the affected table, then use a trigger like this to enforce the logic:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
AFTER UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT 1 FROM INSERTED WHERE childcount>5)
BEGIN
RAISERROR('Count of children can not exceed 5',16,1)
ROLLBACK
RETURN
END
GO
If you just want to ignore the update for any rows that violate the rule try this:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
INSTEAD OF UPDATE
AS
SET NOCOUNT ON
UPDATE y
SET col1=i.col1
,col2=i.col2
,col3=i.col3
,.... --list all columns except the PK column!
FROM dbo.YourTable y
INNER JOIN INSERTED i on y.PK=i.PK
WHERE i.childcount<=5
GO
It will only update rows that have a child count less than 5, ignoring all affected rows that fail the requirement (no error message).
This article from microsoft explains the syntax of creating triggers.
http://msdn.microsoft.com/en-us/library/ms189799.aspx
There isn't really a 'before' trigger, but you can use an INSTEAD OF trigger that allows you to jump in place of whatever action is attempted, then define your own action.
I've used that technique for versioning data.
CREATE TRIGGER [dbo].[Documents_CreateVersion]
ON [dbo].[Documents]
INSTEAD OF UPDATE
AS
BEGIN
DECLARE #DocumentID int
SELECT DocumentID = DocumentID FROM INSERTED
-- do something
END
INSERTED is a bit of a misnomer here, but it contains the details of the action before it occurs you can then define your own action with that data.
Edit:
As per comments below my response, my example can be dangerous if multiple rows are updated at once. My application doesn't allow for this so it's fine in this case. I would agree that the above is a bad practice regardless.
to drop trigger- use database_name
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'tgr_name' AND type = 'TR')
DROP TRIGGER tgr_name
GO
Here's a simple trigger that checks columns values, and fires before updating or inserting, and raises an error.
IF OBJECT_ID ('dbo.MyTableTrigger', 'TR') IS NOT NULL
DROP TRIGGER dbo.MyTableTrigger;
GO
CREATE TRIGGER MyTableTrigger
ON dbo.MyTable
FOR INSERT, UPDATE
AS
DECLARE #Col1ID INT
DECLARE #Col2ID INT
SELECT #Col1ID = Col1ID, #Col2ID = Col2ID FROM inserted
IF ((#Col1ID IS NOT NULL) AND (#Col2ID IS NOT NULL))
BEGIN
RAISERROR ('Col1ID and Col2ID cannot both be in MyTable at the same time.', 16, 10);
END