Insert trigger doesnt do what i want it to do - sql

i made a trigger which should avoid inserting a record in the rental 'uitlening' table if the person has an overdue payment (Boete). Unfortunately it doesnt work and i cant find the reason why. 'Boete' is an attribute of another table than rental. Can someone help me?
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [dbo].[Uitlening]
FOR INSERT
AS
BEGIN
DECLARE #Boete decimal(10, 2);
SET #Boete = (SELECT Boete FROM Lid WHERE LidNr = (SELECT LidNr FROM inserted));
IF #Boete = 0
BEGIN
INSERT INTO Uitlening
SELECT *
FROM inserted;
END;
END;

It sounds like what you actually need is a cross-table constraint.
You can either do this by throwing an error in the trigger:
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [Rental]
AFTER INSERT
AS
SET NOCOUNT ON;
IF EXISTS (SELECT 1
FROM inserted i
INNER JOIN dbo.Person p ON i.[personID] = p.[personID]
WHERE p.[PaymentDue] <= 0
)
THROW 50001, 'PaymentDue is less than 0', 1;
A better solution is to utilize a trick with an indexed view. This is based on an article by spaghettidba.
We first create a dummy table of two rows
CREATE TABLE dbo.DummyTwoRows (dummy bit not null);
INSERT DummyTwoRows (dummy) VALUES(0),(1);
Then we create the following view:
CREATE VIEW dbo.vwPaymentLessThanZero
WITH SCHEMBINDING -- needs schema-binding
AS
SELECT 1 AS DummyOne
FROM dbo.Rental r
JOIN dbo.Person p ON p.personID = r.personID
CROSS JOIN dbo.DummyTwoRows dtr
WHERE p.PaymentDue <= 0;
This view should in theory always have no rows in it. To enforce that, we create an index on it:
CREATE UNIQUE CLUSTERED INDEX CX_vwPaymentLessThanZero
ON dbo.vwPaymentLessThanZero (DummyOne);
Now if you try to add a row that qualifies for the view, it will fail with a unique violation, because the cross-join is doubling up the rows.
Note that in practice the view index takes up no space because there are never any rows in it.

Assuming you just want to insert records into [Rental] of those users, who have [PaymentDue] <= 0. As you mentioned in your last comment:
no record in rental can be inserted if the person has a PaymentDue
thats greater than zero
And other records should be silently discarded as you didn't give a clear answer to #Larnu's question:
should that row be silently discarded, or should an error be thrown?
If above assumptions are true, your trigger would look like:
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [Rental]
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO [Rental] ( [DATE], [personID], [productID])
SELECT i.[DATE], i.[personID], i.[productID]
FROM INSERTED i
INNER JOIN Person p ON i.[personID] = p.[personID]
WHERE p.[PaymentDue] <= 0
END;

Attention! When you create a trigger by FOR INSERT or AFTER INSERT then don't write insert into table select * from inserted, because DB will insert data automatically, you can do only ROLLBACK this process. But, when creating a trigger by INSTEAD OF INSERT then you must write insert into table select * from inserted, else inserting not be doing.

Related

Prevent duplicate data in using After Insert Trigger

I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.

How to get a inserted id in other table from inside a trigger?

I have 3 tables tbl_Users, tbl_Protocol and tbl_ProtocolDetails and inside of my trigger on Users, I have to inserted into Protocol and then insert into ProtocolDetails, but I don't know how work the inserted scope.
Something like that:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
DECLARE #UserId = Int
DECLARE #ProtocolId = Int
DECLARE #UserDetail = NVARCHAR(255)
SELECT
#UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
FROM INSERTED
INSERT INTO tbl_Protocol (user_id, inserted_date)
VALUES (#UserId, GetDate())
-- Return Inserted Id from tbl_Protocol into #ProtocolDetail then
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
VALUES (#ProtocolId, #UserDetail)
END
Your trigger has a MAJOR flaw in that you seems to expect to always have just a single row in the Inserted table - that is not the case, since the trigger will be called once per statement (not once for each row), so if you insert 20 rows at once, the trigger is called only once, and the Inserted pseudo table contains 20 rows.
Therefore, code like this:
Select #UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
From INSERTED;
will fail, since you'll retrieve only one (arbitrary) row from the Inserted table, and you'll ignore all other rows that might be in Inserted.
You need to take that into account when programming your trigger! You have to do this in a proper, set-based fashion - not row-by-agonizing-row stlye!
Try this code:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
-- declare an internal table variable to hold the inserted "ProtocolId" values
DECLARE #IdTable TABLE (UserId INT, ProtocolId INT);
-- insert into the "tbl_Protocol" table from the "Inserted" pseudo table
-- keep track of the inserted new ID values in the #IdTable
INSERT INTO tbl_Protocol (user_id, inserted_date)
OUTPUT Inserted.user_id, Inserted.ProtocolId INTO #IdTable(UserId, ProtocolId)
SELECT user_id, SYSDATETIME()
FROM Inserted;
-- insert into the "tbl_ProtocolDetails" table from both the #IdTable,
-- as well as the "Inserted" pseudo table, to get all the necessary values
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
SELECT
t.ProtocolId,
i.user_detail + '#' + i.user_explanation
FROM
#IdTable t
INNER JOIN
Inserted i ON i.user_id = t.UserId
END
There is nothing in this trigger that would handle a multiple insert/update statement. You will need to either use one scenario that will handle multiple records or check how many records were effected with a IF ##ROWCOUNT = 1 else statement. In your example, I would just use something like
insert into tbl_Protocol(user_id, inserted_date)
select user_id, user_detail + '#' + user_explanation
From INSERTED;
As for your detail table, I see Marc corrected his answer to include the multiple lines and has a simple solution or you can create a second trigger on the tbl_Protocol. Another solution I have used in the past is a temp table for processing when I have very complicated triggers.

Insert multiple rows into a table with a trigger on insert into another table

I am attempting to create a T-SQL trigger that will essentially insert x number of rows into a third table based upon the data being inserted into the original table and data contained in a second table; however, I'm getting all sorts of errors in the select portions of the insert statement.
If I comment out this portion [qmgmt].[dbo].[skillColumns].[columnID] in (select columnID from [qmgmt].[dbo].[skillColumns]), IntelliSense gets rid of all the red lines.
Table designs:
Users table - contains user info
skillColumns table - list of all columns possible, filtered on the productGroupID of the user
Skills table - contains the data per user, one row for every columnID in skillColumns
CREATE TRIGGER tr_Users_INSERT
ON [qmgmt].[dbo].[Users]
AFTER INSERT
AS
BEGIN
INSERT into [qmgmt].[dbo].[Skills]([userID], [displayName], [columnID])
Select [iTable].[userID],
[iTable].[displayName],
[cID] in (select [columnID] as [cID] from [qmgmt].[dbo].[skillColumns])
From inserted as [iTable] inner join
[qmgmt].[dbo].[skillColumns] on
[iTable].[productGroupID] = [qmgmt].[dbo].[skillColumns].[groupID]
END
GO
Is what I'm looking to accomplish even possible with a trigger? Can multiple rows be inserted into a table with the in keyword?
UPDATE:
After using the answer provided by J0e3gan, I was able to create a trigger in the opposite direction:
CREATE TRIGGER tr_skillColumns_INSERT_Users
ON [qmgmt].[dbo].[skillColumns]
AFTER INSERT
AS
BEGIN
INTO [qmgmt].[dbo].[Skills]([userID], [displayName], [columnID])
Select [qmgmt].[dbo].[Users].[userID],
[qmgmt].[dbo].[Users].[displayName],
[iTable].[columnID]
From inserted as [iTable] inner Join
[qmgmt].[dbo].[Users] on
[iTable].[groupID] = [qmgmt].[dbo].[Users].[productGroupID]
Where
[qmgmt].[dbo].[Users].[userID] in (select [userID] from [qmgmt].[dbo].[Users])
END
GO
Yes, this can be done with an AFTER trigger.
The column list is not the correct place for the IN criterion that you are trying to use, which is why it is underlined in red.
Try adding the IN criterion to the JOIN criteria instead:
CREATE TRIGGER tr_Users_INSERT
ON [qmgmt].[dbo].[Users]
AFTER INSERT
AS
BEGIN
INSERT into [qmgmt].[dbo].[Skills]([userID], [displayName], [columnID])
Select [iTable].[userID],
[iTable].[displayName],
[qmgmt].[dbo].[skillColumns].[columnID]
From inserted as [iTable] inner join
[qmgmt].[dbo].[skillColumns] on
[iTable].[productGroupID] = [qmgmt].[dbo].[skillColumns].[groupID] and
[qmgmt].[dbo].[skillColumns].[columnID] in (select columnID from [qmgmt].[dbo].[skillColumns])
END
GO
Alternatively add it to a WHERE clause:
CREATE TRIGGER tr_Users_INSERT
ON [qmgmt].[dbo].[Users]
AFTER INSERT
AS
BEGIN
INSERT into [qmgmt].[dbo].[Skills]([userID], [displayName], [columnID])
Select [iTable].[userID],
[iTable].[displayName],
[qmgmt].[dbo].[skillColumns].[columnID]
From inserted as [iTable] inner join
[qmgmt].[dbo].[skillColumns] on
[iTable].[productGroupID] = [qmgmt].[dbo].[skillColumns].[groupID]
Where
[qmgmt].[dbo].[skillColumns].[columnID] in (select columnID from [qmgmt].[dbo].[skillColumns])
END
GO

Insert data into table when i am using trigger?

Here is a trigger
CREATE TRIGGER [dbo].[CheckApplyId]
ON [dbo].[AppliedStudent_event] INSTEAD OF INSERT
AS
DECLARE #studentId INT
DECLARE #compReq_Id INT
BEGIN
SELECT #studentId = studentId
FROM INSERTED
SELECT #compReq_Id = compReq_Id
FROM INSERTED
IF EXISTS(SELECT StudentId,
compreq_id
FROM AppliedStudent_event
WHERE StudentId = #studentId
AND compreq_id = #compReq_Id)
BEGIN
ROLLBACK
PRINT 'User Already Applied'
END
END
When in insert a data into a table using command:
INSERT INTO AppliedStudent_event (StudentId, compreq_id)
VALUES (3026, 1)
Message is:
(1 row(s) affected)
But when I execute a sql command no data is inserted in the table.
Can you please tell why are you using trigger because you use only assign the variable #studentId and #compReq_Id from inserted table.
That's a broken trigger because inserted can contain multiple (or no) rows - so a statement like SELECT #ScalarVariable = column from inserted is always wrong.
And it's unnecessary since you can just place a UNIQUE constraint on the StudentId and compreq_id columns:
ALTER TABLE AppliedStudent_event
ADD CONSTRAINT UQ_Student_Events
UNIQUE (StudentId,compreq_id)
And it's further broken because you've specified it as an instead of trigger - that says that your code is going to be responsible for the actual insert - but your code doesn't actually do that. That's why no data ends up in the table.
If you insist on doing it as a trigger, it's actually tricky to get everything correct (that's why I'd really recommend the UNIQUE constraint). It'll end up being something like this:
CREATE TRIGGER [dbo].[CheckApplyId]
ON [dbo].[AppliedStudent_event] INSTEAD OF INSERT
AS
IF EXISTS(select
StudentId,compreq_id,COUNT(*)
from inserted
group by StudentId,compreq_id
HAVING COUNT(*) > 1)
OR EXISTS (select *
from inserted i
inner join
AppliedStudent_event e
on
i.StudentId = e.StudentId and
i.compreq_id = e.compreq_id)
BEGIN
ROLLBACK
PRINT 'User Already Applied'
END
ELSE
BEGIN
INSERT INTO AppliedStudent_event(StudentId,compreq_id /* Other columns? */)
SELECT StudentId,compreq_id /* And again, other columns */
FROM inserted
END

Insert into a temporary table and update another table in one SQL query (Oracle)

Here's what I'm trying to do:
1) Insert into a temp table some values from an original table
INSERT INTO temp_table SELECT id FROM original WHERE status='t'
2) Update the original table
UPDATE original SET valid='t' WHERE status='t'
3) Select based on a join between the two tables
SELECT * FROM original WHERE temp_table.id = original.id
Is there a way to combine steps 1 and 2?
You can combine the steps by doing the update in PL/SQL and using the RETURNING clause to get the updated ids into a PL/SQL table.
EDIT:
If you still need to do the final query, you can still use this method to insert into the temp_table; although depending on what that last query is for, there may be other ways of achieving what you want. To illustrate:
DECLARE
id_table_t IS TABLE OF original.id%TYPE INDEX BY PLS_INTEGER;
id_table id_table_t;
BEGIN
UPDATE original SET valid='t' WHERE status='t'
RETURNING id INTO id_table;
FORALL i IN 1..id_table.COUNT
INSERT INTO temp_table
VALUES (id_table(i));
END;
/
SELECT * FROM original WHERE temp_table.id = original.id;
No, DML statements can not be mixed.
There's a MERGE statement, but it's only for operations on a single table.
Maybe create a TRIGGER wich fires after inserting into a temp_table and updates the original
Create a cursor holding the values from insert and then loop through the cursor updating the table. No need to create temp table in the first place.
You can combine steps 1 and 2 using a MERGE statement and DML error logging. Select twice as many rows, update half of them, and force the other half to fail and then be inserted into an error log that you can use as your temporary table.
The solution below assumes that you have a primary key constraint on ID, but there are other ways you could force a failure.
Although I think this is pretty cool, I would recommend you not use it. It looks very weird, has some strange issues (the inserts into TEMP_TABLE are auto-committed), and is probably very slow.
--Create ORIGINAL table for testing.
--Primary key will be intentionally violated later.
create table original (id number, status varchar2(10), valid varchar2(10)
,primary key (id));
--Create TEMP_TABLE as error log. There will be some extra columns generated.
begin
dbms_errlog.create_error_log(dml_table_name => 'ORIGINAL'
,err_log_table_name => 'TEMP_TABLE');
end;
/
--Test data
insert into original values(1, 't', null);
insert into original values(2, 't', null);
insert into original values(3, 's', null);
commit;
--Update rows in ORIGINAL and also insert those updated rows to TEMP_TABLE.
merge into original original1
using
(
--Duplicate the rows. Only choose rows with the relevant status.
select id, status, valid, rownumber
from original
cross join
(select 1 rownumber from dual union all select 2 rownumber from dual)
where status = 't'
) original2
on (original1.id = original2.id and original2.rownumber = 1)
--Only math half the rows, those with rownumber = 1.
when matched then update set valid = 't'
--The other half will be inserted. Inserting ID causes a PK error and will
--insert the data into the error table, TEMP_TABLE.
when not matched then insert(original1.id, original1.status, original1.valid)
values(original2.id, original2.status, original2.valid)
log errors into temp_table reject limit 999999999;
--Expected: ORIGINAL rows 1 and 2 have VALID = 't'.
--TEMP_TABLE has the two original values for ID 1 and 2.
select * from original;
select * from temp_table;