Trigger is re-triggered by internal select on same table - sql-server

I'm having this very strange issue that took me hours just to discover. My code worked perfectly during test with static #Start and #End variables defined, but within the trigger, these are being updated unexpectedly and the cause is the line in the first block of code that inserts values into #tbl.
I suspect this must be because I am selecting values from the very same table the trigger is for. However, since this is only a select and not an update I would have never expected this behavior.
I need to accomplish the following: When a task is updated, I need to check all other tasks for the Chore to see if they are completed and if so, perform inner logic.
I would deeply appreciate some insight here. I've been stuck on this for days now. :(
CREATE TRIGGER ut_Task_Update_PC ON tblTasks
AFTER UPDATE
AS
--Global Vars
declare #Start int, #End int, #ChoreID int
set #ChoreID = (select ChoreID from inserted)
set #Start = (select PC from inserted) -- PC = PercentComplete
set #End = (select PC from deleted)
print('1A: Start = '+cast(#Start as varchar(12))+', End = '+cast(#End as varchar(12)))
--Logic
if (#Start=100) and (#End=100) begin
print('Do nothing: Case 1.')
end else if (#Start=100) and (#End<100) begin
print('Do nothing: Case 2.')
end else if (#Start>0) and (#End<100) begin
print('Do nothing: Case 3.')
end else begin
declare #tbl table (PC int)
print('2A: Start = '+cast(#Start as varchar(12))+', End = '+cast(#End as varchar(12)))
--PROBLEM HERE: inserting explicit value = ok; inserting from same table
-- referenced by trigger somehow triggers another update but only once.
--insert #tbl values (1)
insert #tbl select isnull(PC,0) as [PC] from tblTasks
where ChoreID = #ChoreID and IsCancelled = 0
print('2B: Start = '+cast(#Start as varchar(12))+', End = '+cast(#End as varchar(12)))
if not exists (select null from #tbl where PC = 0)
and (select count(*) from #tbl) > 0 begin --DESIRED CASE
--Never reached
end
end
If the problem is not immediate obvious, you might try the below code for testing.
--Testing: use this sample table and add the above trigger
-- I trimmed away a LOT of extraneous code, but this should
-- reflect the same problems I'm having
CREATE TABLE tblTask(
TaskID int IDENTITY(1,1) NOT NULL,
ChoreID int NOT NULL,
PC int,
IsCancelled bit
)

Related

Trigger causes error (subquery return more than one value) on Bulk Insert

Alter Trigger [dbo].[DiscountUpdate]
on [dbo].[t_PromoDtl]
Instead of insert
as
begin
Declare #Barcode nvarchar(25);
Declare #disper decimal(18,0);
Declare #status int;
Declare #BranchID nvarchar(15);
set #Barcode = (Select barcodeFull from inserted); ---/// I think error happens in here.
set #disper = (Select disPer from inserted); ---/// I think error happens in here.
set #status = (Select p.status from inserted p); ---/// I think error happens in here.
begin
if #status = 2
begin
update t_Prd
set PrdDiscnt = #disper
where BarcodeFull = #Barcode;
end
else
begin
update t_Prd
set PrdDiscnt = 0
where BarcodeFull = #Barcode;
end
end
end
Here is my C# code..
using (var sqlBulk3 = new SqlBulkCopy(_connectionString, SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.CheckConstraints))
{
using (SqlConnection con6 = new SqlConnection(_connectionString))
{
con6.Open();
SqlCommand cmdtt = new SqlCommand("Truncate Table t_PromoDtl", con6);
cmdtt.CommandType = CommandType.Text;
cmdtt.ExecuteNonQuery();
con6.Close();
}
sqlBulk3.DestinationTableName = "t_PromoDtl";
sqlBulk3.WriteToServer(PromoDtl);
}
When Bulk insert starts, the trigger throws this error:
Sub query returns more than one value....
I looked at this trigger which updates t_Prd table instead of insert on t_PromoDtl table.
set #Barcode = (Select barcodeFull from inserted); ---/// I think error happens in here.
set #disper = (Select disPer from inserted); ---/// I think error happens in here.
set #status = (Select p.status from inserted p); ---/// I think error happens in here.
You seem to assume that the SQL Server trigger will be fired separately for each row - this is NOT the case - the trigger is fired only once for a statement. And if this is a BULK INSERT, then the Inserted pseudo table will contain multiple rows - so your statements like
set #Barcode = (Select barcodeFull from inserted);
are in fact the source of the problem - which one of the 250 rows inserted are you selecting here? It's not determined - you'll get back one arbitrary row - and what happens to the other 249 rows also inserted?? They're just plain ignored and not handled.
You need to rewrite your entire trigger logic to be set-based and handle the fact that the Inserted pseudo table will most likely contain multiple rows.
Try something like this:
ALTER TRIGGER [dbo].[DiscountUpdate]
ON [dbo].[t_PromoDtl]
INSTEAD OF INSERT
AS
BEGIN
-- update "dbo.T_Prd.PrdDiscnt" to "disPer" when status is 2
UPDATE p
SET PrdDiscnt = i.disPer
FROM dbo.T_Prd p
INNER JOIN Inserted i ON i.BarcodeFull = p.BarcodeFull
WHERE i.Status = 2;
-- update "dbo.T_Prd.PrdDiscnt" to "0" when status is not 2
UPDATE p
SET PrdDiscnt = 0
FROM dbo.T_Prd p
INNER JOIN Inserted i ON i.BarcodeFull = p.BarcodeFull
WHERE i.Status <> 2;
I'm assuming here that BarcodeFull is your primary key column that uniquely identifies each row in your table - if that's not the case, you might need to adapt the JOIN condition to match your situation.

SQL Trigger Inconsistently firing

I have a SQL Trigger on a table that works... most of the time. And I cannot figure out why sometimes the fields are NULL
The trigger works by Updateing the LastUpdateTime whenever something is modified in the field, and the InsertDatetime when first Created.
For some reason this only seems to work some times.
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS(SELECT * FROM DELETED)
BEGIN
UPDATE ES
SET InsertDatetime = Getdate()
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
IF UPDATE(LastUpdateDateTime) OR UPDATE(InsertDatetime)
RETURN;
IF EXISTS (
SELECT
*
FROM
INSERTED I
JOIN
DELETED D
-- make sure to compare inserted with (same) deleted person
ON D.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
)
BEGIN
UPDATE ES
SET InsertDatetime = ISNULL(es.Insertdatetime,Getdate())
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
END
A much simpler and efficient approach to do what you are trying to do, would be something like...
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Determine if this is an INSERT OR UPDATE Action .
DECLARE #Action as char(1);
SET #Action = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set Action to Insert.
END);
UPDATE ES
SET InsertDatetime = CASE WHEN #Action = 'U'
THEN ISNULL(es.Insertdatetime,Getdate())
ELSE Getdate()
END
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER;
END
"If update()" is poorly defined/implemented in sql server IMO. It does not do what is implied. The function only determines if the column was set by a value in the triggering statement. For an insert, every column is implicitly (if not explicitly) assigned a value. Therefore it is not useful in an insert trigger and difficult to use in a single trigger that supports both inserts and updates. Sometimes it is better to write separate triggers.
Are you aware of recursive triggers? An insert statement will execute your trigger which updates the same table. This causes the trigger to execute again, etc. Is the (database) recursive trigger option off (which is typical) or adjust your logic to support that?
What are your expectations for the insert/update/merge statements against this table? This goes back to your requirements. Is the trigger to ignore any attempt to set the datetime columns directly and set them within the trigger always?
And lastly, what exactly does "works sometimes" actually mean? Do you have a test case that reproduces your issue. If you don't, then you can't really "fix" the logic without a specific failure case. But the above comments should give you sufficient clues. To be honest, your logic seems to be overly complicated. I'll add that it also is logically flawed in the way that it set insertdatetime to getdate if the existing value is null during an update. IMO, it should reject any update that attempts to set the value to null because that is overwriting a fact that should never change. M.Ali has provided an example that is usable but includes the created timestamp problem. Below is an example that demonstrates a different path (assuming the recursive trigger option is off). It does not include the rejection logic - which you should consider. Notice the output of the merge execution carefully.
use tempdb;
set nocount on;
go
create table zork (id integer identity(1, 1) not null primary key,
descr varchar(20) not null default('zippy'),
created datetime null, modified datetime null);
go
create trigger zorktgr on zork for insert, update as
begin
declare #rc int = ##rowcount;
if #rc = 0 return;
set nocount on;
if update(created)
select 'created column updated', #rc as rc;
else
select 'created column NOT updated', #rc as rc;
if exists (select * from deleted) -- update :: do not rely on ##rowcount
update zork set modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
else
update zork set created = getdate(), modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
end;
go
insert zork default values;
select * from zork;
insert zork (descr) values ('bonk');
select * from zork;
update zork set created = null, descr = 'upd #1' where id = 1;
select * from zork;
update zork set descr = 'upd #2' where id = 1;
select * from zork;
waitfor delay '00:00:02';
merge zork as tgt
using (select 1 as id, 'zippity' as descr union all select 5, 'who me?') as src
on tgt.id = src.id
when matched then update set descr = src.descr
when not matched then insert (descr) values (src.descr)
;
select * from zork;
go
drop table zork;

Resume a WHILE loop from where it stopped SQL

I have a while loop query that I only want to run until 11PM everyday - I'm aware this can be achieved with a WAITFOR statement, and then just END the query.
However, on the following day, once I re-run my query, I want it to continue from where it stopped on the last run. So I'm thinking of creating a log table that will contain the last processed ID.
How can I achieve this?
DECLARE #MAX_Value BIGINT = ( SELECT MAX(ID) FROM dbo.TableA )
DECLARE #MIN_Value BIGINT = ( SELECT MIN(ID) FROM dbo.TableA )
WHILE (#MIN_Value < #MAX_Value )
BEGIN
INSERT INTO dbo.MyResults
/* Do some processing*/
….
….
….
SET #MIN_Value = MIN_Value + 1
/*I only want the above processing to run until 11PM*/
/* Once it’s 11PM, I want to save the last used #MIN_Value
into my LoggingTable (dbo.Logging) and kill the above processing.*/
/* Once I re-run the query I want my processing to restart from the
above #MIN_Value which is recorded in dbo.Logging */
END
Disclaimer: I do not recommend using WHILE loops in SQL Server but considering the comment that you want a solution in SQL, here you go:
-- First of all, I strongly recommend using a different way of assigning variable values to avoid scenarios with the variable being NULL when the table is empty, also you can do it in a single select.
-- Also, if something started running at 10:59:59 it will let the processing for the value finish and will not simply rollback at 11.
CREATE TABLE dbo.ProcessingValueLog (
LogEntryId BIGINT IDENTITY(1,1) NOT NULL,
LastUsedValue BIGINT NOT NULL,
LastUsedDateTime DATETIME NOT NULL DEFAULT(GETDATE()),
CompletedProcessing BIT NOT NULL DEFAULT(0)
)
DECLARE #MAX_Value BIGINT = 0;
DECLARE #MIN_Value BIGINT = 0;
SELECT
#MIN_Value = MIN(ID),
#MAX_Value = MAX(ID)
FROM
dbo.TableA
SELECT TOP 1
#MIN_Value = LastUsedValue
FROM
dbo.ProcessingValueLog
WHERE
CompletedProcessing = 1
ORDER BY
LastUsedDateTime DESC
DECLARE #CurrentHour TINYINT = HOUR(GETDATE());
DECLARE #LogEntryID BIGINT;
WHILE (#MIN_Value < #MAX_Value AND #CurrentHour < 23)
BEGIN
INSERT INTO dbo.ProcessingValueLog (LastUsedValue)
VALUE(#MIN_Value)
SELECT #LogEntryID = SCOPE_IDENTITY()
// Do some processing...
SET #MIN_Value = #MIN_Value + 1;
UPDATE dbo.ProcessingValueLog
SET CompletedProcessing = 1
WHERE LogEntryId = #LogEntryID
SET #CurrentHour = HOUR(GETDATE())
END

How do you pass identify value to out parameter for an UPDATE?

So far I have something like the following.
However I'm not sure what to do when I perform UPDATE - from another question here I found that you need to store the OUTPUT INSERTED result to a table because the update (or insert) may affect multiple rows? I tried using SCOPE IDENTITY but it return NULL on the UPDATE. Anyway if I use the table - then how do I get an individual integer that I can pass to the out parameter? Or do I have change the out parameter to a different type like a collection?
ALTER PROCEDURE [Data].[UpdateRecord]
#theValue decimal(4,2) = NULL,
#updatetime datetimeoffset(7),
#maxintervaltime datetimeoffset(7),
#recordID int = NULL output
AS
declare #mytable as TABLE
(
Id int
)
begin tran
if exists (select * from Data.theValue with (updlock,serializable) where Data.theValue.maxintervaltime = #maxintervaltime)
begin
update Data.theValue set theValue = #theValue, updatetime = #updatetime, maxintervaltime = #maxintervaltime
where Data.theValue.maxintervaltime = #maxintervaltime
-- OUTPUT INSERTED.id into #mytable (this line is wrong)
end
else
begin
insert into Data.theValue(theValue, updatetime, maxintervaltime) values(#theValue, #updatetime, #maxintervaltime);
SET #recordID = SCOPE_IDENTITY();
end
commit tran
The output clause should be placed between update and from/where clause. UPDATE can affect multi rows so you have to ensure your logic is correct.
update Data.theValue set theValue = #theValue, updatetime = #updatetime, maxintervaltime = #maxintervaltime
OUTPUT INSERTED.id into #mytable
where Data.theValue.maxintervaltime = #maxintervaltime
SET #recordID = top 1 id from #mytable

TSQL Trigger Not Saving Variables and/or not Executing Properly

I've having trouble getting a TSQL trigger to even work correctly. I've run it through the debugger and it's not setting any of the variables according to SQL Server Management Studio. The damnedest thing is that the trigger itself is executing correctly and there are no errors when it is executed (just says 'execution successful').
The code is as follows (it's a work in progress.... just getting my self familiar):
USE TestDb
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'OfficeSalesQuotaUpdate' AND type = 'TR')
DROP TRIGGER OfficeSalesQuotaUpdate
GO
CREATE TRIGGER OfficeSalesQuotaUpdate
ON SalesReps
AFTER UPDATE, DELETE, INSERT
AS
DECLARE #sales_difference int, #quota_difference int
DECLARE #sales_original int, #quota_original int
DECLARE #sales_new int, #quota_new int
DECLARE #officeid int
DECLARE #salesrepid int
--UPDATE(Sales) returns true for INSERT and UPDATE.
--Not for DELETE though.
IF ((SELECT COUNT(*) FROM inserted) = 0)
SET #salesrepid = (SELECT SalesRep FROM deleted)
ELSE
SET #salesrepid = (SELECT SalesRep FROM inserted)
--If you address the #salesrepid variable, it does not work. Doesn't even
--print out the 'this should work line.
PRINT 'This should work...' --+ convert(char(30), #salesrepid)
IF (#salesrepid = NULL)
PRINT 'SalesRepId is null'
ELSE
PRINT 'SalesRepId is not null'
PRINT convert(char(50), #salesrepid)
SET #officeid = (SELECT RepOffice
FROM SalesReps
WHERE SalesRep = #salesrepid)
SELECT #sales_original = (SELECT Sales FROM deleted)
SELECT #sales_new = (SELECT Sales FROM inserted)
--Sales can not be null, so we'll remove this later.
--Use this as a template for quota though, since that can be null.
IF (#sales_new = null)
BEGIN
SET #sales_new = 0
END
IF (#sales_original = 0)
BEGIN
SET #sales_original = 0
END
SET #sales_difference = #sales_new - #sales_original
UPDATE Offices
SET Sales = Sales + #sales_difference
WHERE Offices.Office = #officeid
GO
So, any tips? I've completely stumped on this one. Thanks in advance.
Your main problem seems to be that there is a difference between #foo = NULL and #foo IS NULL:
declare #i int
set #i = null -- redundant, but explicit
if #i = null print 'equals'
if #i is null print 'is'
The 'This should work' PRINT statement doesn't work because concatenating a NULL with a string gives a NULL, and PRINT NULL doesn't print anything.
As for actually setting the value of #salerepid, it seems most likely that the inserted and/or deleted table is in fact empty. What statements are you using to test the trigger? And have you printed out the COUNT(*) value?
You should also consider (if you haven't already) what happens if someone changes more than one row at once. Your current code assumes that only one row is changed at a time, which may be a reasonable assumption in your environment, but it can easily break if someone bulk loads data or does other 'batch processing'.
Finally, you should always mention your MSSQL version and edition; it can be relevant for some syntax questions.
You should replace the body of the trigger with something like this:
;WITH Totals AS (
SELECT RepOffice,SUM(Sales) as Sales FROM inserted GROUP BY RepOffice
UNION ALL
SELECT RepOffice,-SUM(Sales) FROM deleted GROUP BY RepOffice
), SalesDelta AS (
SELECT RepOffice,SUM(Sales) as Delta FROM Totals GROUP BY RepOffice
)
UPDATE o
SET Sales = Sales + sd.Delta
FROM
Offices o
inner join
SalesDelta sd
on
o.Office = sd.RepOffice
This will adequately cope with multiple rows in inserted and deleted. I'm assuming SalesRep is the primary key of the SalesReps table.
Updated above, to cope with UPDATE changing the RepOffice of a particular Sales Rep (which the original doesn't, presumable, get correct either)
Just a suggestion...have you tried putting BEGIN and END to encapsulate the 'AS' part of your trigger?

Resources