Trigger to ensure each new bid is higher than previous - sql

I have a problem with my trigger in SQL Server.
This trigger checks if the new bidding is higher then the existing ones and if not raise an error:
ALTER TRIGGER [dbo].[trg_bod_validate_Bodbedrag]
ON [dbo].[bod]
FOR INSERT, UPDATE
AS
DECLARE #v_Voorwerp numeric(25);
DECLARE #v_Bodbedrag char(6);
DECLARE #v_Max_Bodbedrag char(6);
select #v_Voorwerp = i.voorwerp, #v_Bodbedrag = i.bodbedrag
from Inserted i;
SELECT #v_Max_Bodbedrag = max(CAST(bodbedrag AS INT))
FROM bod
WHERE voorwerp = #v_Voorwerp;
IF #v_Max_Bodbedrag <= #v_Bodbedrag
BEGIN
RAISERROR ('Bod moet hoger zijn dan gegeven bod.', 16, 1)
ROLLBACK TRANSACTION
END;
ELSE
PRINT 'Row Inserted';
Now I get this error Bid amount is less then maximum, that is not acceptable', even when I insert a bidding when there aren't any existing bids.
What could be the problem?
For your knowledge: Voorwerp: Product, Bodbedrag: Amount of bid

Points to note:
In SQL Server the Inserted (and Deleted) pseudo-tables can have from 0-N rows where N is the number of rows being inserted/updated/deleted. And this has to be handled in any trigger. However as soon as you switch into set-based thinking (instead of procedural) you find its a much simpler problem anyway.
Of course we can't (easily) discriminate between rows which are OK and those which aren't, we basically have to reject the entire insert/update even if its just one row which breaks the rules.
So the join finds the max(bodbedrag) for all products which exist in Inserted - excluding the row which is being inserted/updated - because as far as we are concerned the insert/update has already taken place and that data exists in our database - until we rollback if we choose to.
I've ignored your use of a char instead of a decimal. Ideally you would correct your datatypes, but you can continue to cast/convert if you wish. I'll leave that to you.
Note we use throw these days, not raiserror.
ALTER TRIGGER [dbo].[trg_bod_validate_Bodbedrag]
ON [dbo].[bod]
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Find the max of for inserted voorwerp's excluding the currently inserted/updated
-- Is an update of existing bid even allowed
IF EXISTS (
SELECT 1
FROM Inserted I
LEFT JOIN (
SELECT MAX(bodbedrag) bodbedrag
FROM dbo.dob D
INNER JOIN Inserted I ON I.voorwerp = D.voorwerp and I.ID <> D.ID
) D
WHERE I.bodbedrag < coalesce(D.bodbedrag,0)
) BEGIN
-- THROW should be used now, not RAISERROR
-- RAISERROR ('Bod moet hoger zijn dan gegeven bod.', 16, 1);
THROW 51000, 'Bod moet hoger zijn dan gegeven bod.', 1;
ROLLBACK TRANSACTION;
END; ELSE BEGIN
PRINT 'Row Inserted';
END;
END

Related

Prevent duplicates with trigger SQL

My objective is inserting the first insert, but not letting the second to pass, because NIC is duplicated. I don't know why, but it isn't letting the first one pass without having other NIC to compare if it already exists one equal.
I know I can prevent duplicates with "unique", but I was trying to do with a trigger :/
Create table Utentes
(
numUtente nchar(3),
Name nchar(40) not null,
NIC nchar(8) not null,
Constraint PK_Utente Primary Key(numUtente)
)
create trigger tr_Duplicate
on Utentes
after insert
as
declare #NIC nchar(8)
select #NIC = NIC from inserted
if exists(select * from Utentes where NIC = #NIC)
begin
print 'NIC already in database'
rollback
end
go
insert into Utentes (numUtente, Name, NIC)
values ('123', 'asd', '12345678')
insert into Utentes (numUtente, Name, NIC)
values ('124', 'asd', '12345678')
select * from Utentes
Result:
NIC already in database
Msg 3609, Level 16, State 1, Line 1392
The transaction ended in the trigger. The batch has been aborted.
You should really use a constraint. An "after insert" trigger will actually put the second row in the table . . . and hopefully no one is using NOLOCK for reading it.
In any case, you have to actually count the rows and look for multiple occurrences. It would be something like this:
Create trigger tr_Duplicate on Utentes after INSERT as
begin
if exists (select 1
from utentes u join
inserted i
on u.nic = i.nic
group by u.nic
having count(*) > 1
)
begin
print 'NIC already in database';
rollback;
end;
end;
With an instead of trigger, you would not add new rows into the table if one already exists:
create trigger tr_Duplicate on Utentes after INSERT as
begin
if exists (select 1
from utentes u join
inserted i
on u.nic = i.nic
)
begin
print 'NIC already in database';
rollback;
end;
else
begin
insert into utentes
select i.*
from inserted i;
end;
end;
I would second the sentiment against the use of triggers and would also suggest using UNIQUE constraints. In my humble opinion, I would rather search for a solution in the ETL layer, grouping records as they are inserted. With triggers you will get the aforementioned concurrency and consistency issues, as well as potentially swelling your tempdb or T-log if the table ever gets big enough to take some time to process.

use insert value as trigger

is it possible to use the insert value in the where clause in SQL?
I would want to use it in this kinda way:
create trigger t_wage_not_higher_than_30000
on transaction
as
if exists
(
select *
from transaction
where ***inserted value*** >= 30000 and
description = employeewage
)
begin
raiserror('you cannot insert a wage higher than 30000')
rollback transaction
end
If you want to check the range of values, the best way is to use a check constraint:
alter table transactions add constraint chk_transactions_value
check (value < 30000);
There is no reason to write a trigger for checking data values.
For this trigger, you should only refer to the newly inserted rows. Therefore, you need to use the special trigger table in your statement. E.g.,
if exists (select * from inserted
where <some column you did not name in
your code> >= 30000 and description = 'employeewage')
begin
raiserror ... (or throw);
rollback tran;
return;
end;
And yes - a constraint is generally a better approach.

Stored procedure for referential integrity between two tables in different database?

For two tables A and B, I'd like to implement referential integrity such that in tables A a foreign key's value must present in table B, while in table B a primary key can only be deleted or modified if that value does not present in table A. My requirement is that I'd like to have table A, and B as variable, and apply the procedure to any arbitrary instances of tables. That is,
sp_referential_integrity_across_databases(A, B)
I have figured out how to do the referential integrity as triggers for a pair of particular tables. I wonder if it's feasible to write such stored procedure to save future effort?
My environment is Microsoft SQL Server 2017. The more portable the solution, the better.
Here are my crafted procedures:
The triggers on table "A" for insert and update:
USE DWPractice
IF OBJECT_ID ('dbo.trgCheckCustomer_Cat_Id_Customer_D', 'TR') IS NOT NULL
DROP Trigger trgCheckCustomer_Cat_Id_Customer_D;
GO
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_D
ON Customer_D
AFTER INSERT, UPDATE
AS
IF NOT EXISTS
(
SELECT Customer_Cat_Id
FROM inserted
WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [OtherDW].[dbo].[Customer_Cat_D])
)
BEGIN
RAISERROR('Lookup Value Not Found -- Inerst Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
The trigger on table "B" for delete and update:
USE OtherDW
IF OBJECT_ID ('dbo.trgCheckCustomer_Cat_Id_Customer_Cat_D', 'TR') IS NOT NULL
DROP Trigger trgCheckCustomer_Cat_Id_Customer_Cat_D;
GO
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_Cat_D
ON Customer_Cat_D
AFTER DELETE, UPDATE
AS
Begin
IF EXISTS
(
SELECT Customer_Cat_Id
FROM deleted
WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [DWPractice].[dbo].[Customer_D])
)
BEGIN
RAISERROR('Lookup Value Found -- Delete Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
-- It seems that the following for the case of update is not needed
-- The above clauses would get executed even for the case of update.
-- IF EXISTS
-- (
-- SELECT Customer_Cat_Id
-- FROM inserted
-- WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [DWPractice].[dbo].[Customer_D])
-- )
-- BEGIN
-- RAISERROR('Lookup Value Found -- Update Failed', 16, 1);
-- ROLLBACK TRANSACTION;
-- END;
End;
If stored procedure is not the best practice, then what is the best practice? It seems to me, there's much boiler-plate code, with only database name and table name are variables.
The logic in your (first) trigger is not correct. If you have multiple rows in inserted, then only one has to match. Instead, you want:
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_D ON Customer_D AFTER INSERT, UPDATE
AS BEGIN
IF EXISTS (SELECT 1
FROM inserted i LEFT JOIN
[OtherDW].[dbo].[Customer_Cat_D] d
ON i.Customer_Cat_Id = d.Customer_Cat_Id
WHERE d.Customer_Cat_Id IS NULL
)
BEGIN
RAISERROR('Lookup Value Not Found -- Insert Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
END; -- trigger

Rollback under condition in MariaDB

I have a transaction which reduces the variable with amount of money in loop, if the variable with money is below 0, the money amount should return to the value before transaction. How can I appropriately use rollback in MariaDB in this case?
---edit
I have something like that, and it doesn't work, check out the lines in if(budget<0) because if the money is below 0 and some, but not all of them, iterations were made and saved to the temp table, the table shows them
BEGIN
DECLARE temppesel text;
DECLARE tempsalary int;
DECLARE budget int DEFAULT cash;
DECLARE done bool DEFAULT false;
DECLARE occ CURSOR FOR (SELECT pesel, pensja FROM pracownicy where zawod=occupation);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
START TRANSACTION;
DROP TABLE IF EXISTS temp;
CREATE TABLE temp ( Result text );
OPEN occ;
occ : LOOP
FETCH occ INTO temppesel, tempsalary;
SET budget = budget - tempsalary;
IF(done) THEN
LEAVE occ;
END IF;
IF(budget<0) THEN
ROLLBACK;
LEAVE occ;
END IF;
INSERT INTO temp VALUES (concat('********',substr(temppesel,9,3), ', wyplacono'));
END LOOP;
CLOSE occ;
SELECT * FROM temp;
DROP TABLE temp;
COMMIT;
END
I believe that the CREATE TABLE statements are causing the transaction to be committed. Here is a list of commands that cause an implicit COMMIT.
As the aforementioned link describes, you can either move the START TRANSACTION statement after the DROP and CREATE commands or use the CREATE TEMPORARY TABLE syntax to create a temporary table:
CREATE TEMPORARY TABLE temp ( Result text );
BEGIN;
do some SQL
Loop:
do some SQL
if something is wrong, ROLLBACK and exit the loop and transaction
do some SQL
if something, go back to Loop
do some SQL
COMMIT;
That is, let ROLLBACK undo everything since the BEGIN.
More
Now that the SP is visible...
What engine is temp? If it MyISAM, then it is not rolled back. SHOW VARIABLES LIKE 'default_storage_engine';.
Please don't use occ for 2 different things, it confuses the reader.
Do you want the output to be part of the rows of pracownicy when the budget is blown? Or do you want no rows?
If you have multiple connections doing the same thing, there is a serious problem -- temp is visible to all connections, and they could step on each other. Change to CREATE TEMPORARY TABLE temp ...
However, with the pre-test (below), you can completely avoid the use of temp. First test for its need, then (if needed) simply do a single SELECT for all the rows.
If you want nothing, then a simple test something like this would pre-test whether it will overflow, therey obviating the need for testing in the loop:
..
IF ( SELECT SUM(pensja)
FROM pracownicy
where zawod=occupation ) > budget )

PL/SQL Triggers Library Infotainment System

I am trying to make a Library Infotainment System using PL/SQL. Before any of you speculate, yes it is a homework assignment but I've tried hard and asking a question here only after trying hard enough.
Basically, I have few tables, two of which are:
Issue(Bookid, borrowerid, issuedate, returndate) and
Borrower(borrowerid, name, status).
The status in Borrower table can be either 'student' or 'faculty'. I have to implement a restriction using trigger, that per student, I can issue only 2 books at any point of time and per faculty, 3 books at any point of time.
I am totally new to PL/SQL. It might be easy, and I have an idea of how to do it. This is the best I could do. Please help me in finding design/compiler errors.
CREATE OR REPLACE TRIGGER trg_maxbooks
AFTER INSERT ON ISSUE
FOR EACH ROW
DECLARE
BORROWERCOUNT INTEGER;
SORF VARCHAR2(20);
BEGIN
SELECT COUNT(*) INTO BORROWERCOUNT
FROM ISSUE
WHERE BORROWER_ID = :NEW.BORROWER_ID;
SELECT STATUS INTO SORF
FROM BORROWER
WHERE BORROWER_ID = :NEW.BORROWER_ID;
IF ((BORROWERCOUNT=2 AND SORF='STUDENT')
OR (BORROWERCOUNT=3 AND SORF='FACULTY')) THEN
ROLLBACK TRANSACTION;
END IF;
END;
Try something like this:
CREATE OR REPLACE TRIGGER TRG_MAXBOOKS
BEFORE INSERT
ON ISSUE
FOR EACH ROW
BEGIN
IF ( :NEW.BORROWERCOUNT > 2
AND :NEW.SORF = 'STUDENT' )
OR ( :NEW.BORROWERCOUNT > 3
AND :NEW.SORF = 'FACULTY' )
THEN
RAISE_APPLICATION_ERROR (
-20001,
'Cannot issue beyond the limit, retry as per the limit' );
END IF;
END;
/
There should not be a commit or rollback inside a trigger. The logical exception is equivalent to ROLLBACK
This is so ugly I can't believe you're being asked to do something like this. Triggers are one of the worst ways to implement business logic. They will often fail utterly when confronted with more than one user. They are also hard to debug because they have hard-to-anticipate side effects.
In your example for instance what happens if two people insert at the same time? (hint: they won't see the each other's modification until they both commit, nice way to generate corrupt data :)
Furthermore, as you are probably aware, you can't reference other rows of a table inside a row level trigger (this will raise a mutating error).
That being said, in your case you could use an extra column in Borrower to record the number of books being borrowed. You'll have to make sure that the trigger correctly updates this value. This will also take care of the multi-user problem since as you know only one session can update a single row at the same time. So only one person could update a borrower's count at the same time.
This should help you with the insert trigger (you'll also need a delete trigger and to be on the safe side an update trigger in case someone updates Issue.borrowerid):
CREATE OR REPLACE TRIGGER issue_borrower_trg
AFTER INSERT ON issue
FOR EACH ROW
DECLARE
l_total_borrowed NUMBER;
l_status borrower.status%type;
BEGIN
SELECT nvl(total_borrowed, 0) + 1, status
INTO l_total_borrowed, l_status
FROM borrower
WHERE borrower_id = :new.borrower_id
FOR UPDATE;
-- business rule
IF l_status = 'student' and l_total_borrowed >= 3
/* OR faculty */ THEN
raise_application_error(-20001, 'limit reached!');
END IF;
UPDATE borrower
SET total_borrowed = l_total_borrowed
WHERE borrower_id = :new.borrower_id;
END;
Update: the above approach won't even work in your case because you record the issue date/return date in the issue table so the number of books borrowed is not a constant over time. In that case I would go with a table-level POST-DML trigger. After each DML verify that every row in the table validates your business rules (it won't scale nicely though, for a solution that scales, see this post by Tom Kyte).