DML Trigger for Insert or Update to fire conditionally - sql

I'm trying to create a trigger on table MainDataTable that will insert a row to table EPR conditionally.
These are the conditions.
If an insert is made to table MainDataTable and inserts are enabled in table EPRCheck then do the insert into EPR.
If an update is made to table MainDataTable and updates are enabled in table EPRCheck then do the insert into EPR.
What do I need to do to the WHERE clause in the trigger to get this to work, or maybe I need to have two separate triggers for inserts and updates?
This is the EPRCheck table
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'EPRCheck')
BEGIN
DROP TABLE EPRCheck
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE EPRCheck
([TriggerTypeID] [int] NOT NULL,
[TriggerTypeDesc] [varchar](6) NOT NULL,
[IsEnabled] [bit] NOT NULL,
CONSTRAINT [PK_EPRCheck_TriggerTypeID] PRIMARY KEY CLUSTERED
(
[TriggerTypeID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]
GO
IF NOT EXISTS (SELECT * FROM EPRCheck WHERE TriggerTypeID = 1)
BEGIN
INSERT INTO EPRCheck
VALUES (1, 'Insert', 1)
END
GO
IF NOT EXISTS (SELECT * FROM EPRCheck WHERE TriggerTypeID = 2)
BEGIN
INSERT INTO EPRCheck
VALUES (2, 'Update', 1)
END
GO
IF NOT EXISTS (SELECT * FROM [dbo].[EPRCheck] WHERE TriggerTypeID = 3)
BEGIN
INSERT INTO EPRCheck
VALUES (3, 'Delete', 1)
END
GO
and this is the trigger
IF EXISTS (SELECT 1 FROM sys.triggers WHERE [name] = 'TriggerEPRInsertUpdate')
BEGIN
DROP TRIGGER TriggerEPRInsertUpdate
END
GO
--Trigger Type
--INSERT = 1
--UPDATE = 2
--DELETE = 3
CREATE TRIGGER TriggerEPRInsertUpdate ON MainDataTable
AFTER INSERT, UPDATE
NOT FOR REPLICATION
AS
DECLARE #insertIsEnabled bit, #updateIsEnabled bit
SET #insertIsEnabled = (SELECT IsEnabled FROM EPRCheck WHERE TriggerTypeID = 1)
SET #updateIsEnabled = (SELECT IsEnabled FROM EPRCheck WHERE TriggerTypeID = 2)
IF #insertIsEnabled = 1 OR #updateIsEnabled = 1
BEGIN
DECLARE #triggerTypeID int
IF EXISTS (SELECT 0 FROM inserted)
BEGIN
IF EXISTS (SELECT 0 FROM deleted)
BEGIN
SELECT #triggerTypeID = 2
END
ELSE
BEGIN
SELECT #triggerTypeID = 1
END
END
ELSE
BEGIN
SELECT #triggerTypeID = 3
END
IF (#triggerTypeID = 1 AND #insertIsEnabled = 1) OR (#triggerTypeID = 2 AND #updateIsEnabled = 1)
BEGIN
DECLARE #createDateTime datetime
SET #createDateTime = GETDATE()
SET XACT_ABORT ON;
SET NOCOUNT ON;
BEGIN TRANSACTION
INSERT INTO EPR
SELECT
NEWID() AS DocumentID,
GETDATE() AS CreateDateTime,
NULL AS ProcessDateTime,
#triggerTypeID AS TriggerTypeID,
1 AS MessageStatus
FROM inserted i
LEFT JOIN deleted d ON i.ID = d.ID
WHERE
(#insertIsEnabled = 1 AND NOT EXISTS (SELECT i.Status, i.KeyDate, i.OtherDate, i.KeyCode, i.OtherCode
EXCEPT
SELECT d.Status, d.KeyDate, d.OtherDate, d.KeyCode, d.OtherCode))
OR
(#updateIsEnabled = 1 AND EXISTS (SELECT i.Status, i.KeyDate, i.OtherDate, i.KeyCode, i.OtherCode
EXCEPT
SELECT d.Status, d.KeyDate, d.OtherDate, d.KeyCode, d.OtherCode))
COMMIT TRANSACTION
END
END

Your issue is here:
WHERE (#insertIsEnabled = 1 AND NOT EXISTS (SELECT i.Status, i.KeyDate, i.OtherDate, i.KeyCode, i.OtherCode
EXCEPT
SELECT d.Status, d.KeyDate, d.OtherDate, d.KeyCode, d.OtherCode))
And specifically with the NOT EXISTS. For all inserted events your deleted records will be null, therefore unless your inserted records are all null you'll always have a record returned, therefore the not exist will return false. You can either change EXCEPT to INTERSECT or change to EXISTS - both will mean this works as required:
Example on db<>fiddle
With that being said, I'd still be inclined to slightly rewrite this dealing with your two scenarios separately, and do away with all the IF/ELSE logic at the start too:
CREATE TRIGGER TriggerEPRInsertUpdate ON dbo.MainDataTable
AFTER INSERT, UPDATE
AS
BEGIN
INSERT INTO EPR (DocumentID, CreatedDateTime, TriggerTypeID, MessageStatus)
SELECT DocumentID = NEWID(), CreateDateTime = GETDATE(), TriggerTypeID = epr.TriggerTypeId, MessageStatus = 1
FROM inserted AS i
INNER JOIN EPRCheck AS epr
ON epr.TriggerTypeDesc = 'Insert'
AND epr.IsEnabled = 1
WHERE NOT EXISTS (SELECT 1 FROM Deleted AS d WHERE d.ID = i.ID)
UNION ALL
SELECT DocumentID = NEWID(), CreateDateTime = GETDATE(), TriggerTypeID = epr.TriggerTypeId, MessageStatus = 1
FROM inserted AS i
INNER JOIN deleted AS d
ON i.ID = d.ID
INNER JOIN EPRCheck AS epr
ON epr.TriggerTypeDesc = 'Update'
AND epr.IsEnabled = 1
WHERE NOT EXISTS
( SELECT i.Status, i.KeyDate, i.OtherDate, i.KeyCode, i.OtherCode
INTERSECT
SELECT d.Status, d.KeyDate, d.OtherDate, d.KeyCode, d.OtherCode
);
END
Example on db<>fiddle

Related

SQL Trigger DATETIME Constraint

I need to create a trigger in a SQL table that inserts data that passes a condition, [value] = 2, into a separate table. However, I do not want to insert new data from the same source that occurs in the same half hour.
That is, data with [ID] = 1 is inserted into [table1] and passes [value] = 2. This data is then inserted into [table2]. If [ID] = 1 produces another line of data with [value] = 2 in the same half hour, it should not be inserted into [table2].
Here is what I have so far:
CREATE TRIGGER [dbo].[example_trigger] ON [dbo].[table1]
AFTER INSERT
AS
BEGIN
DECLARE
#alert_ID varchar(MAX),
#alert_created_at datetime2(7),
#alert_value numeric(8,2)
SELECT #alert_ID = INSERTED.ID,
#alert_created_at = INSERTED.created_at,
#alert_value = INSERTED.value FROM INSERTED
IF #alert_value = 2
AND #alert_created_at > DATEADD(minute, 30, (SELECT MAX([alert_created_at]) FROM [table2] WHERE #alert_uid = [alert_uid]))
BEGIN
INSERT INTO table2(alert_uid, alert_created_at, alert_leak_status)
VALUES(#alert_uid, #alert_created_at, #alert_leak_status)
END

Foreach row, update another table row if exist else insert a new record

I have a query which returns a number of rows.
In following code, one EnrollmentId can have multiple StudentIds.
SELECT
[StudentId]
, [ennrollmentId]
, [Name]
, [StartDate]
, [Enddate]
FROM
[Enrollements]
WHERE
[ennrollmentId] = #ennrollmentId;
Now I want to insert StudentId, EnrollemntId, StartDate and EndDate into a new table with these rules, if no row exists for StudentId and EnrollmentId; if such a row already exists, then update the StartDate and EndDate.
I wrote following query:
INSERT INTO [AnotherTable]
SELECT *
FROM [Enrollements]
WHERE [enrollmentId] = #enrollmentId;
-- I have enrollmentId as stored procedure parameter
But this always inserts a new record, it does not update.
Here is the non-merge solution
-- Do the update FIRST, because if you INSERT first, then afterwards ALL records will match
UPDATE e1 set [StartDate]=e2.[StartDate],[Enddate]=e2.[Enddate]
FROM [Enrollements] e1
INNER JOIN [Enrollements] e2 on e2.[ennrollmentId]=e1.[ennrollmentId] and e2.[StudentId]=e1.[StudentId]
INSERT INTO [AnotherTable]
SELECT
*
FROM [Enrollements] e1
WHERE NOT EXISTS
(
SELECT 1
FROM [Enrollements] e2
WHERE e2.[ennrollmentId]=e1.[ennrollmentId] and e2.[StudentId]=e1.[StudentId]
)
-- Update the row if it exists.
UPDATE at
SET at.StartDate= en.StartDate
, at.EndDate = en.EndDate -- Edited: "," in SQL Server in place of "AND"
FROM [AnotherTable] at
join [Enrollements] en
ON (en.[ennrollmentId] = at.[ennrollmentId] AND en.[ennrollmentId] = #ennrollmentId)
-- Insert the row if the UPDATE statement failed.
IF (##ROWCOUNT = 0 )
BEGIN
INSERT INTO [AnotherTable]
SELECT *
FROM [Enrollements]
WHERE [enrollmentId] = #enrollmentId; -- EDITED: updated the table,column name
END

Triggers execution after group update with "in (...)" statement in query

I have AFTER UPDATE trigger for in table:
ALTER TRIGGER [dbo].[table1]
ON [dbo].[table]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #primaryKey bigint
SELECT #PrimaryKey = PK FROM Inserted
if EXISTS(select * from [dbo].[table1] where PK=#PrimaryKey)
begin
update [dbo].[table1] set [Action] = 'U' where PK=#PrimaryKey
end
else
begin
insert into [dbo].[table1] ([PK], [Action], StampIn)
values (#PrimaryKey, 'U', GETDATE())
end
END
When I do "update SOME_DB.dbo.TABLE set FIELD='NEW VALUE' where PK in (3,4,5)", I found that only one row was added to the table1 with PK "3". Which means that trigger was executed only once in table.
But I need to have all the rows in table1 with PKs that were updated.
Could you please help me to solve my problem?
Thank you.
SQL triggers use the inserted view to identify all the rows being inserted. Your logic is only looking at one of the rows; hence it does not do what you expect. So:
BEGIN
SET NOCOUNT ON;
update t1
set [Action] = 'U'
from table1 t1 join
inserted i
on i.primarykey = t1.pk ;
insert into [dbo].[table1] ([PK], [Action], StampIn)
select i.primarykey, 'U', getdate()
from inserted i
where not exists (select 1 from dbo.table1 t1 where t1.pk = i.primarykey);
END;
You don't actually need the conditional logic, because the join and where clauses take care of that.

SQL script took too much time and throws a time out error

I am executing SQL script from my application. Using this script I am taking data from Product table and putting it into Table1. If I execute this script directly from SSMS it will take 00:01:27 minute. But using application it gives me an error: Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
Here is my script:
-- Deleting [Table1] table if exists before creating because we don't need to keep track of records
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'Table1'))
BEGIN
DROP TABLE [dbo].[Table1]
END
-- Creating [Table1] table
CREATE TABLE [dbo].[Table1]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ProductId] [int] NOT NULL,
[MyStatus] [bit] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[InTime] [datetime] NULL,
[StoreId] [int] NOT NULL,
[LanguageId] [int] NOT NULL,
PRIMARY KEY([Id])
);
DECLARE #pId int
DECLARE #sId int
Truncate Table [dbo].[Table1]
Insert Into [dbo].[Table1] (ProductId, MyStatus, IsDeleted, InTime, StoreId, LanguageId)
select distinct
p.Id, 1, 1, GETDATE(), s.Id, l.Id
from
[dbo].[Store] s, [dbo].[Product] p, [dbo].[Language] as l
left join
[dbo].[StoreMapping] sm on sm.EntityId = l.Id
where
(l.LimitedToStores = 1 and s.Id in (sm.StoreId)
and sm.EntityName = 'Language') or l.LimitedToStores = 0
DECLARE tempCursor CURSOR SCROLL FOR
select
p.Id, sm.StoreId
from
[dbo].[Product] as p
join
[dbo].[StoreMapping] as sm on p.Id = sm.EntityId
join
[dbo].[Store] as s on sm.StoreId=s.Id
where
p.LimitedToStores = 1 and sm.EntityName = 'Product'
OPEN tempCursor
FETCH FIRST FROM tempCursor INTO #pId, #sId
WHILE ##fetch_status = 0
BEGIN
UPDATE [dbo].[Table1]
SET IsDeleted = 0
WHERE ProductId = #pId AND StoreId = #sId
FETCH NEXT FROM tempCursor INTO #pId, #sId;
END
CLOSE tempCursor
UPDATE [dbo].[Table1]
SET IsDeleted = 0
where ProductId in (Select p.Id
from [dbo].[Product] as p
where p.LimitedToStores = 0)
UPDATE [dbo].[Table1]
SET IsDeleted = 1
where ProductId in (Select p.Id
from [dbo].[Product] as p
where p.Published = 0 OR p.Deleted = 1
OR p.VisibleIndividually = 0)
DEALLOCATE tempCursor
Can anyone help me to improve this script?
The part with the CURSOR and looping through it to set IsDeleted=0 in Table1 can be replaced with the following update statement:
UPDATE
[dbo].[Table1]
SET
IsDeleted=0
FROM
[dbo].[Product] AS p
JOIN [dbo].[StoreMapping] AS sm ON p.Id=sm.EntityId
JOIN [dbo].[Store] AS s ON sm.StoreId=s.Id
JOIN [dbo].[Table1] AS t1 ON t1.ProductId=p.Id AND t1.StoreId=s.Id
WHERE
p.LimitedToStores=1 AND
sm.EntityName ='Product'
The last two queries are better written with JOINs
UPDATE
[dbo].[Table1]
SET
IsDeleted=0
FROM
[dbo].[Table1] AS t1
JOIN [dbo].[Product] AS p ON
p.Id=t1.ProductId
WHERE
p.LimitedToStores=0
UPDATE
[dbo].[Table1]
SET
IsDeleted=1
FROM
[dbo].[Table1] AS t1
JOIN [dbo].[Product] AS p ON
p.Id=t1.ProductId
WHERE
p.Published=0 OR
p.Deleted=1 OR
p.VisibleIndividually=0

How To Get A Hierarchical CTE In SQL Server To Filter With Parent and Child Logic

I'm having a vexing problem with a hierarchical CTE and some strange logic that we need to address that I really hope someone could assist with pointing out what I'm doing wrong to address this scenario with a CTE.
Here is the hierarchical data we're dealing with in this example:
This is the problematic SQL followed by the description of the problem and SQL statements to create a test table with data:
DECLARE #UserId nvarchar(50);
SET #UserId = 'A';
DECLARE #StatusType int;
SET #StatusType = '2';
;WITH recursiveItems (Id, Depth)
AS
(
SELECT Id, 0 AS Depth
FROM dbo.CteTest
WHERE UserId = #UserId
--AND StatusType = #StatusType
-- This would also be incorrect for the issue
AND ParentId IS NULL
UNION ALL
SELECT dbo.CteTest.Id, Depth + 1
FROM dbo.CteTest
INNER JOIN recursiveItems
ON dbo.CteTest.ParentId = recursiveItems.Id
WHERE UserId = #UserId
AND StatusType = #StatusType
)
SELECT A.*, recursiveItems.Depth
FROM recursiveItems
INNER JOIN dbo.CteTest A WITH(NOLOCK) ON
recursiveItems.Id = A.Id
ORDER BY A.Id
This is not returning the desired data. The data that is currently returned is in the NOT CORRECT section of the image below. The row with the Id of 10 is the row that we want to omit.
Essentially the logic should be that any parent record (record with children) where the status type of any of its children is equal to 2 should be returned along with its children. In the example this is the rows with Ids: 1, 5, 6, 7, 9.
Currently the CTE/SQL/Code is returning ALL parent records no matter what,
The record with the Id 1 should be returned, even though it's status type is 1 because at least one of its children, their children, grandchildren, etc. have a status type that is equal to 2.
The record with the Id of 10 should not be returned because it does not have a status that is equal to 2 or any children. If the record had a status type of 2 when it has no child records it should also be returned.
This is the DDL to create a test table that helps to show the problem:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CteTest](
[Id] [int] IDENTITY(1,1) NOT NULL,
[StatusType] [int] NOT NULL,
[UserId] [nvarchar](50) NOT NULL,
[ParentId] [int] NULL,
CONSTRAINT [PK_CteTest] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
This is the seed data for the table, that can demonstrate the issue:
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (1,'A',NULL)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (1,'B',NULL)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (2,'B',NULL)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (1,'A',1)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (2,'A',1)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (2,'A',5)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (2,'A',6)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (3,'A',6)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (2,'A',NULL)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (4,'A',NULL)
INSERT INTO [dbo].[CteTest]([StatusType],[UserId],[ParentId]) VALUES (3,'A',10)
The issue is that your base case includes all null (parentless) items, and there is no way to filter them out later.
Because you are looking for only items with a particular statustype, you may want to refactor the CTE; Instead of having a base case be the root values, you can have it be all items with the given statustype, and then recursively find the parents. In the solution below, I have depth be a negative number, for distance from the item with a value of 2 in the given tree (so negative height, instead of depth.).
DECLARE #UserId nvarchar(50);
SET #UserId = 'A';
DECLARE #StatusType int;
SET #StatusType = '2';
WITH recursiveItems (Id, ParentID, Depth)
AS
(
SELECT Id, ParentID, 0 AS Depth
FROM dbo.CteTest
WHERE UserId = #UserId AND StatusType = #StatusType
UNION ALL
SELECT dbo.CteTest.Id, CteTest.ParentID, Depth - 1
FROM dbo.CteTest
INNER JOIN recursiveItems
ON dbo.CteTest.Id = recursiveItems.ParentId
WHERE UserId = #UserId
)
SELECT A.Id, A.StatusType, A.UserId, A.ParentId, min(recursiveItems.Depth)
FROM recursiveItems
INNER JOIN dbo.CteTest A WITH(NOLOCK) ON
recursiveItems.Id = A.Id
group by A.Id, A.StatusType, A.UserId, A.ParentId
ORDER BY A.Id