Trigger on status change - sql

I need to write a trigger in SQL, first of all I show you my table structure
CREATE TABLE ZAMOW
(
IDZAMOW int primary key identity(1,1) not null,
IDKLIENTA int not null REFERENCES KLIENT(IDKLIENTA),
DATA DATE not null DEFAULT(GETDATE()),
STATUS char(1) CHECK(STATUS = 'P' OR STATUS = 'W' OR STATUS = 'Z' OR STATUS = 'A' )DEFAULT('P')
)
CREATE TABLE ZAMOCZESCI
(
IDZAMOW int not null REFERENCES ZAMOW(IDZAMOW),
IDCZESCI int not null REFERENCES CZESC(IDCZESCI),
ILOSC float not null
)
CREATE TABLE CZESC
(
IDCZESCI int primary key identity(1,1) not null,
NAZWA char(30) not null CHECK ((datalength([NAZWA])>(3))),
OPIS char(200) DEFAULT('Brak opisu'),
CENA decimal(10,2) not null
)
CREATE TABLE MAGACZESCI
(
IDMAGAZYNU int not null REFERENCES MAGAZYN(IDMAGAZYNU),
IDCZESCI int not null REFERENCES CZESC(IDCZESCI),
ILOSC float not null
)
I want to create a trigger that will trigger only if ZAMOW.STATUS changes to 'W' or 'Z' and then will subtract all values MAGACZESCI.ILOSC = MAGACZESCI.ILOSC - ZAMOCZESCI.ILOSC identifying id by IDCZESCI
For example if I have in table MAGACZESCI values (1,1,5) and (1,2,5) // 5 pieces of part number 1 and 2, in table ZAMOW(1,1,currentdate,'P'),
and in table ZAMOCZESCI (1,1,3), (1,2,2) 3 pieces of part 1 and 2 pieces of part 2
I want to trigger only if status changes from 'P' -> 'W' OR 'Z'
and then to change values in MAGACZESCI to (1,1,5-3) and (1,2,5-2) identifying by IDCZESCI
This example is for 2 rows but I want it to be more flexible, sometimes even for 100 or more rows
I came up with something like this
CREATE TRIGGER STATUSCHANGE
ON ZAMOW
AFTER UPDATE
AS
IF UPDATE(STATUS)
IF (ZAMOW.STATUS = 'Z' OR ZAMOW.STATUS = 'W')
DECLARE #idczesci int
DECLARE #ilosc float
DECLARE C1 CURSOR FOR SELECT ZAMOCZESCI.IDCZESCI,ZAMOCZESCI.ILOSC FROM ZAMOCZESCI WHERE ZAMOCZESCI.IDZAMOW = ZAMOW.IDZAMOW
OPEN C1
FETCH NEXT FROM C1 INTO #idczesci,#ilosc
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE MAGACZESCI
SET ILOSC = ILOSC - #ilosc
WHERE IDCZESCI = #idczesci
END
GO
But I don't know how to tell SQL that IF (ZAMOW.STATUS = 'Z' OR ZAMOW.STATUS = 'W') id for rows that are updated, it tells me couldn't bound because of multipart identifier.

This is incomplete as in your attempt you reference the columns that don't exist in the table ZAMOW yet that is what the trigger is on. I suspect that is because we have incomplete DDL as you have keys on tables like MAGAZYN that don't exist in the DDL you provide.
Anyway, this should be enough to get you where you need to be, however, I can't test it, as the columns ILOSC and idczesci don't exist in your sample DDL:
CREATE TRIGGER trg_StatusChange ON dbo.ZAMOW
AFTER UPDATE AS
BEGIN
IF UPDATE ([Status]) BEGIN
UPDATE M
SET ILOSC = ILOSC - i.ILOSC --This column doesn't exist in your table, so where is it coming from?
FROM dbo.MAGACZESCI M
JOIN inserted i ON M.IDCZESCI = i.idczesci --This column doesn't exist in your table, so where is it coming from?
JOIN deleted d ON i.IDZAMOW = d.IDZAMOW --This column doesn't exist in your table, so where is it coming from?
WHERE d.[STATUS] = 'P'
AND i.[status] != 'P';
END;
END;

..just a try...maybe you get something out of it..
drop table if exists ZAMOCZESCI_test
go
drop table if exists MAGACZESCI_test
go
drop table if exists ZAMOW_test
go
drop table if exists CZESC_test
go
CREATE TABLE ZAMOW_test
(
IDZAMOW int primary key identity(1,1) not null,
IDKLIENTA int not null, -- REFERENCES KLIENT(IDKLIENTA),
DATA DATE not null DEFAULT(GETDATE()),
STATUS char(1) CHECK(STATUS = 'P' OR STATUS = 'W' OR STATUS = 'Z' OR STATUS = 'A' )DEFAULT('P')
)
go
insert into ZAMOW_test(IDKLIENTA)
values(510), (520),(530);
go
CREATE TABLE CZESC_test
(
IDCZESCI int primary key identity(1,1) not null,
NAZWA char(30) not null CHECK ((datalength([NAZWA])>(3))),
OPIS char(200) DEFAULT('Brak opisu'),
CENA decimal(10,2) not null
)
go
insert into CZESC_test(NAZWA, CENA)
values('A123', 4), ('B567', 3), ('C009', 7),
--
('X001', 150), ('X002', 500), ('X003', 700)
;
go
CREATE TABLE ZAMOCZESCI_test
(
IDZAMOW int not null REFERENCES ZAMOW_test(IDZAMOW),
IDCZESCI int not null REFERENCES CZESC_test(IDCZESCI),
ILOSC float not null
)
go
insert into ZAMOCZESCI_test(IDZAMOW, IDCZESCI, ILOSC)
values(1, 1, 3), (1, 2, 2), (1, 3, 9),
--2nd&3rd clients
(2, 4, 60), (2, 5, 100), (2, 6, 300),
(3, 5, 150), (3, 6, 120);
go
CREATE TABLE MAGACZESCI_test
(
IDMAGAZYNU int not null,-- REFERENCES MAGAZYN(IDMAGAZYNU),
IDCZESCI int not null REFERENCES CZESC_test(IDCZESCI),
ILOSC float not null
)
go
insert into MAGACZESCI_test(IDMAGAZYNU, IDCZESCI, ILOSC)
values (1001, 1, 5), (1002, 2, 5), (1003, 3, 20),
--
(1004, 4, 200), (1005, 5, 1000), (1006, 6, 2000);
--client 2, assigned 60units of prod4, after trigger: prod4 = 200-60=140
--clients 2&3, assigned 100+150 units of prod5, after trigger: prod5 = 1000 - (100+150) = 750
-- prod6, after trigger: prod6 = 2000 - (300+120) = 1580
go
--based on this
/*
select *
from ZAMOW_test as zam --<--(is replaced by deleted&inserted)
join ZAMOCZESCI_test as zamcze on zam.IDZAMOW = zamcze.IDZAMOW --but this needs to get grouped by IDCZESCI, if multiple "orders" change status in a single update
join MAGACZESCI_test as mag on zamcze.IDCZESCI = mag.IDCZESCI
*/
go
create trigger trgUpdateZAMOW_test on ZAMOW_test for /*after*/ update
as
begin
--set nocount on
--set rowcount 0
--if status did not change from p->w or z, do nothing, return/exit
if not exists
(
select *
from deleted as d
join inserted as i on d.IDZAMOW = i.IDZAMOW
where d.STATUS = 'P'
and i.STATUS in ('W', 'Z')
)
begin
return;
end
--get the "orders" which changed from p->w||z, aggregate their products and subtract the product sums from "inventory"?
update mag
set ILOSC = mag.ILOSC - updcze.sumILOSC --what happens if mag.ILOSC - updcze.sumILOSC < 0 ??
--output deleted.*, inserted.*
from
(
select zamcze.IDCZESCI, isnull(sum(zamcze.ILOSC), 0) as sumILOSC
from deleted as d
join inserted as i on d.IDZAMOW = i.IDZAMOW
join ZAMOCZESCI_test as zamcze on d.IDZAMOW = zamcze.IDZAMOW
where d.STATUS = 'P'
and i.STATUS in ('W', 'Z')
group by zamcze.IDCZESCI
having isnull(sum(zamcze.ILOSC), 0) <> 0
) as updcze
join MAGACZESCI_test as mag on updcze.IDCZESCI = mag.IDCZESCI;
end
go
update ZAMOW_test
set STATUS = 'A' --trigger fires and returns/exits, x->A
go
update ZAMOW_test
set STATUS = 'W' --trigger fires and returns/exits, status:from A->W
go
update ZAMOW_test
set STATUS = 'P' --trigger fires, exits, status:from W->P
go
select 'before', *
from MAGACZESCI_test
update ZAMOW_test
set STATUS = 'W' --trigger fires , and updates mag, status: from P->W
select 'after', *
from MAGACZESCI_test;

Related

SQL - "NOT IN" in WHERE clause using INNER JOIN not working

I need to filter a table based in a sub table data.
I'll exemplify with a hypnotic data to be easier to explain:
Master table: Cars
Sub table: Attributes (like Color, car type, accessories)
These attributes have an id (idOption) and the selected value (idList)
So, in an example, I need to filter all the cars with the color (idOption = 10) yellow (idList = 45). I can't filter this directly because the search need to consider the other option's results (which include the types, accessories.
When I use NOT IN for just one table, it works. But when I use merging the 2 tables with INNER JOIN, it does not work.
So in summary, I need to filter the 3 idOption (when is not NULL) with a given value, and this needs to reflect in the main table, grouped by product.
Table Cars:
idProduct | Description
1 Product A
2 Product B
3 Product C
Table Attributes:
idRow idProduct idOption idList
---------------------------------------
1 1 10 45
2 2 10 46
3 3 10 47
4 1 11 10
5 2 11 98
6 1 14 56
7 3 16 28
8 2 20 55
This is the stored procedure that I created which is not working:
ALTER PROCEDURE [dbo].[SP_GET_TestSearch]
(#Param1 BIGINT = NULL,
#PValue1 BIGINT = NULL,
#Param2 BIGINT = NULL,
#PValue2 BIGINT = NULL,
#Param3 BIGINT = NULL,
#PValue3 BIGINT = NULL)
AS
SET NOCOUNT ON;
SELECT
Cars.idProduct,
Cars.[Description]
FROM
Cars
INNER JOIN
Attributes ON Cars.idProduct = Attributes.idProduct
WHERE
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
OR
(idOption = ISNULL(#Param1, NULL)
AND idList = ISNULL(#PValue1, NULL))
OR
(idOption = ISNULL(#Param2, NULL)
AND idList = ISNULL(#PValue2, NULL))
OR
(idOption = ISNULL(#Param3, NULL)
AND idList = ISNULL(#PValue3, NULL))
GROUP BY
Cars.idProduct, Cars.[Description]
The following code demonstrates how to implement the logic of excluding vehicles from query results if they have any "bad" property values. The rejection is handled by ... where not exists ... which is used to check each car against the "bad" property values.
Rather than using an assortment of (hopefully) paired parameters to pass the undesirable properties, the values are passed in a table. The stored procedure to implement this ought to use a table-valued parameter (TVP) to pass the table.
-- Sample data.
declare #Cars as Table ( CarId Int Identity, Description VarChar(16) );
insert into #Cars ( Description ) values
( 'Esplanade' ), ( 'Tankigator' ), ( 'Land Yacht' );
select * from #Cars;
declare #Properties as Table ( PropertyId Int Identity, Description VarChar(16) );
insert into #Properties ( Description ) values
( 'Turbochargers' ), ( 'Superchargers' ), ( 'Hyperchargers' ), ( 'Color' ), ( 'Spare Tires' );
select * from #Properties;
declare #CarProperties as Table ( CarId Int, PropertyId Int, PropertyValue Int );
insert into #CarProperties ( CarId, PropertyId, PropertyValue ) values
( 1, 1, 1 ), ( 1, 4, 24 ), ( 1, 4, 42 ), -- Two tone!
( 2, 2, 1 ), ( 2, 4, 7 ),
( 3, 1, 2 ), ( 3, 4, 0 ), ( 3, 5, 6 );
select C.CarId, C.Description as CarDescription,
P.PropertyId, P.Description as PropertyDescription,
CP.PropertyValue
from #Cars as C inner join
#CarProperties as CP on CP.CarId = C.CarId inner join
#Properties as P on P.PropertyId = CP.PropertyId
order by C.CarId, P.PropertyId;
-- Test data: Avoid vehicles that have _any_ of these property values.
-- This should be passed to the stored procedure as a table-value parameter (TVP).
declare #BadProperties as Table ( PropertyId Int, PropertyValue Int );
insert into #BadProperties ( PropertyId, PropertyValue ) values
( 2, 1 ), ( 2, 2 ), ( 2, 4 ),
( 4, 62 ), ( 4, 666 );
select BP.PropertyId, BP.PropertyValue, P.Description
from #BadProperties as BP inner join
#Properties as P on P.PropertyId = BP.PropertyId;
-- Query the data.
select C.CarId, C.Description as CarDescription
from #Cars as C
where not exists (
select 42
from #CarProperties as CP inner join
#BadProperties as BP on BP.PropertyId = CP.PropertyId and BP.PropertyValue = CP.PropertyValue
where CP.CarId = C.CarId )
order by C.CarId;
A few things here.
Firstly, this kind of catch all procedure is a bit of an anti pattern for all sorts of reasons, see here for a full explanation:- https://sqlinthewild.co.za/index.php/2018/03/13/revisiting-catch-all-queries/
Secondly, you need to be very careful of using NOT IN with nullable values in a list: http://www.sqlbadpractices.com/using-not-in-operator-with-null-values/
I've added the DDL for the tables:-
IF OBJECT_ID('Attributes') IS NOT NULL
DROP TABLE Attributes;
IF OBJECT_ID('Cars') IS NOT NULL
DROP TABLE Cars;
IF OBJECT_ID('SP_GET_TestSearch') IS NOT NULL
DROP PROCEDURE SP_GET_TestSearch
CREATE TABLE Cars
(idProduct INT PRIMARY KEY
, Description VARCHAR(20) NOT NULL);
CREATE TABLE Attributes
(idRow INT PRIMARY KEY
, idProduct INT NOT NULL FOREIGN KEY REFERENCES dbo.Cars(idProduct)
, idOption INT NOT NULL
, idList INT NOT NULL);
INSERT INTO dbo.Cars
VALUES
(1, 'Product A')
,(2 , 'Product B')
,(3, 'Product C');
INSERT INTO dbo.Attributes
(
idRow,
idProduct,
idOption,
idList
)
VALUES (1,1,10,45)
,(2,2,10,46)
,(3,3,10,47)
,(4,1,11,10)
,(5,2,11,98)
,(6,1,14,56)
,(7,3,16,28)
,(8,2,20,55);
GO
The issue with your query, is that the first part of the block is always evaluated to TRUE for any idOption that you don't specify:-
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
To explain; if I pass in the following:-
DECLARE #Param1 BIGINT
, #Param2 BIGINT
, #Param3 BIGINT
, #PValue1 BIGINT
, #PValue2 BIGINT
, #PValue3 BIGINT;
SET #Param1 = 11
SET #Pvalue1 = 42
SET #Param2 = 11
SET #Pvalue2 = 10
SET #Param3 = 14
SET #PValue3= 56
EXEC dbo.SP_GET_TestSearch #Param1, #PValue1, #Param2, #PValue2, #Param3, #PValue3
Then you effectively have WHERE idOption NOT IN (11,14) as the evaluation for the first part of the clause, so all other rows are returned.
I suspect you really want the WHERE clause to be:-
WHERE
(#Param1 IS NULL AND #Param2 IS NULL AND #Param3 IS NULL)
OR
(idOption = #Param1
AND idList = #PValue1)
OR
(idOption = #Param2
AND idList = #PValue2)
OR
(idOption = #Param3
AND idList = #PValue3)

SQL: Update and Insert with condition

I would like to know if this is possible. I would like to update a record only if the typeId equal my value and add a record in table B if that's the case.
TableA:
id (PK, int)
typeId (int)
TableB:
id (PK, int)
tableAId (FK, int)
note (nvarchar)
My SQL script:
UPDATE [dbo].[TableA]
SET [TypeId] = CASE
WHEN [TypeId] = 4 THEN 6 AND
(INSERT INTO [dbo].[TableB] ([tableAId],[note])
VALUES ([dbo].[TableA].Id,'type has changed'))
ELSE [Id]
END
The script above looks like I want to achieve but obviously it's incorrect. How can I do multiple things in my case condition? Update a value and insert a record with the current id?
Data sample:
Table A (id, typeId)
1, 4
2, 5
3, 2
Table B (id, tableAid, note)
1, 1, 'note1'
2, 1, 'note2'
3, 2, 'note1'
Should become:
Table A (id, typeId)
1, 6
2, 5
3, 2
Table B (id, tableAid, note)
1, 1, 'note1'
2, 1, 'note2'
3, 2, 'note1'
4, 1, 'type has changed'
Try to use OUTPUT clause.
Test tables and data:
CREATE TABLE A(
id int NOT NULL PRIMARY KEY,
typeId int NOT NULL
)
INSERT A(id,typeId)VALUES(1, 4),(2, 5),(3, 2)
CREATE TABLE B(
id int NOT NULL IDENTITY PRIMARY KEY,
tableAid int NOT NULL REFERENCES A(id),
note varchar(50) NOT NULL
)
SET IDENTITY_INSERT B ON
INSERT B(id,tableAid,note)VALUES(1, 1, 'note1'),(2, 1, 'note2'),(3, 2, 'note1')
SET IDENTITY_INSERT B OFF
Using OUTPUT demo:
DECLARE #LogTable TABLE(tableAid int,note varchar(50))
UPDATE A
SET
typeId=6
OUTPUT inserted.id,'type has changed'
INTO #LogTable(tableAid,note)
WHERE typeID=4
INSERT B(tableAid,note)
SELECT tableAid,note
FROM #LogTable
If you drop the foreign key in the table B then you can use OUTPUT directly into table B without #LogTable:
-- table B without FOREIGN KEY
CREATE TABLE B(
id int NOT NULL IDENTITY PRIMARY KEY,
tableAid int NOT NULL, --REFERENCES A(id),
note varchar(50) NOT NULL
)
-- only one UPDATE with OUTPUT
UPDATE A
SET
typeId=6
OUTPUT inserted.id,'type has changed'
INTO B(tableAid,note)
WHERE typeID=4

Using the identity column to add a value to a computed column

At times I need to store a temporary value to a field. I have a stored procedure that adds it using:
Insert new record first then
SELECT #Record_Value = SCOPE_IDENTITY();
UPDATE ADMIN_Publication_JSON
SET NonPubID = CAST(#Record_Value as nvarchar(20)) + '_tmp'
WHERE RecID = #Record_Value
It simply takes the identity value and adds an '_tmp' to the end. Is there a way that I can create a default value in the table that would do that automatically if I did not insert a value into that field?
The NonPubID column is just a NVARCHAR(50).
Thanks
You could write a trigger, that replaces NULL with that string upon INSERT.
CREATE TRIGGER admin_publication_json_bi
ON admin_publication_json
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE apj
SET apj.nonpubid = concat(convert(varchar(20), i.id), '_tmp')
FROM admin_publication_json apj
INNER JOIN inserted i
ON i.id = apj.id
WHERE i.nonpubid IS NULL;
END;
db<>fiddle
Downside: You cannot explicitly insert NULLs for that column, should that be desired.
Check out NewKey col below:
CREATE TABLE #Table
(
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
IDValue VARCHAR(1) ,
ModifiedDT DATETIME NULL,
NewKey AS ( CONVERT(VARCHAR(100),ID)+'_Tmp' )
)
INSERT #Table( IDValue, ModifiedDT )
SELECT 'A', GETDATE()
UNION ALL
SELECT 'Y', GETDATE() - 1
UNION ALL
SELECT 'N', GETDATE() - 5
SELECT * FROM #Table

copy a column from one table to another where table1.col = table2.col

Suppose there are two tables which have the data mentioned in the insert query. There is no foreign key references between the two table.
create table uref.slave (
SLAVE_ID SMALLINT NOT NULL PRIMARY KEY,
DESC VARCHAR(20)
);
INSERT INTO uref.SLAVE values (1, null)
INSERT INTO uref.SLAVE values (2, null)
create table uref.master (
MASTER_ID SMALLINT NOT NULL PRIMARY KEY,
SLAVE_ID SMALLINT,
DESC VARCHAR(20)
);
INSERT INTO uref.MASTER values (1,1,'value1')
INSERT INTO uref.MASTER values (2,2,'value2')
Now I need a query which will copy uref.master.DESC into uref.slave.DESC based on uref.master.SLAVE_ID = uref.slave.SLAVE_ID.
The simplest solution may be to use MERGE.
MERGE INTO uref.SLAVE s
USING uref.MASTER m
ON (s.SLAVE_ID = m.SLAVE_ID)
WHEN MATCHED
THEN UPDATE SET Desc = m.Desc
It could be refined to update only when there is a change to be made
MERGE INTO uref.SLAVE s
USING uref.MASTER m
ON (s.SLAVE_ID = m.SLAVE_ID)
WHEN MATCHED
and ( s.Desc <> m.Desc
or (s.Desc is null and m.Desc is not null)
)
THEN UPDATE SET Desc = m.Desc
UPDATE uref.SLAVE t1
SET Desc =
(
SELECT t2.Desc
FROM uref.MASTER t2
WHERE t1.SLAVE_ID = t2.SLAVE_ID
)
WHERE EXISTS
(
SELECT *
FROM uref.MASTER t2
WHERE t1.SLAVE_ID = t2.SLAVE_ID
AND NOT t1.Desc=t2.Desc
)
AND t1.Desc IS NULL
if sql server, Try below sql: (recheck the table name and fields)
declare #urefSlave table (
SLAVE_ID SMALLINT ,
[DESC] VARCHAR(20)
);
INSERT INTO #urefSlave values (1, null)
INSERT INTO #urefSlave values (2, null)
Declare #urefMaster table (
MASTER_ID SMALLINT,
SLAVE_ID SMALLINT,
[DESC] VARCHAR(20)
);
INSERT INTO #urefMaster values (1,1,'value1')
INSERT INTO #urefMaster values (2,2,'value2')
select * from #urefMaster
select * from #urefSlave
update #urefSlave
set [DESC] = b.[DESC]
from #urefSlave a inner join #urefMaster b on a.SLAVE_ID = b.SLAVE_ID
select * from #urefSlave
REsult:
MASTER_ID SLAVE_ID DESC
--------- -------- --------------------
1 1 value1
2 2 value2
SLAVE_ID DESC
-------- --------------------
1 value1
2 value2
Updated
cannot help much in db2, because i don't have the tools to run the syntax
but from this link db2 update help
you can modify an example in there to meet your requirement:
UPDATE EMPLOYEE EU
SET (EU.SALARY, EU.COMM)
=
(SELECT AVG(ES.SALARY), AVG(ES.COMM)
FROM EMPLOYEE ES
WHERE ES.WORKDEPT = EU.WORKDEPT)
WHERE EU.EMPNO = '000120'
Hope this help.

Oracle 10g and data validation in a trigger before row update

I am using Oracle 10g and I have the following table:
create table DE_TRANSFORM_MAP
(
DE_TRANSFORM_MAP_ID NUMBER(10) not null,
CLIENT NUMBER(5) not null,
USE_CASE NUMBER(38) not null,
DE_TRANSFORM_NAME VARCHAR2(100) not null,
IS_ACTIVE NUMBER(1) not null
)
That maps to an entry in the following table:
create table DE_TRANSFORM
(
DE_TRANSFORM_ID NUMBER(10) not null,
NAME VARCHAR2(100) not null,
IS_ACTIVE NUMBER(1) not null
)
I would like to enforce the following rules:
Only one row in DE_TRANSFORM_MAP with the same CLIENT and USE_CASE can have IS_ACTIVE set to 1 at any time
Only one row in DE_TRANSFORM with the same NAME and IS_ACTIVE set to 1 at any time
A row in DE_TRANSFORM cannot have IS_ACTIVE changed from 1 to 0 if any rows in DE_TRANSFORM_MAP have DE_TRANSFORM_NAME equal to NAME and IS_ACTIVE set to 1
Does this make sense?
I have tried to write a stored proc that handles this:
create or replace trigger DETRANSFORMMAP_VALID_TRIG
after insert or update on SERAPH.DE_TRANSFORM_MAP
for each row
declare
active_rows_count NUMBER;
begin
select count(*) into active_rows_count from de_transform_map where client = :new.client and use_case = :new.use_case and is_active = 1;
if :new.is_active = 1 and active_rows_count > 0 then
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
end if;
end;
When I do the following it works as expected, I get an error:
insert into de_transform_map (de_transform_map_id, client, use_case, de_transform_name, is_active) values (detransformmap_id_seq.nextval, 6, 0, 'TEST', 1);
insert into de_transform_map (de_transform_map_id, client, use_case, de_transform_name, is_active) values (detransformmap_id_seq.nextval, 6, 1, 'TEST', 1);
But if I then do this:
update de_transform_map set use_case = 0 where use_case = 1
I get the following:
ORA-04091: table DE_TRANSFORM_MAP is mutating, trigger/function may not see it
How can I accomplish my validation?
EDIT: I marked Rene's answer as correct because I think the most correct and elegant way to do this is with a compound trigger but our production DB is still just 10g, we are updating to 11g early next year and I will rewrite the trigger then. Until then, I have a blanket trigger that will assert that no rows are duplicated, here it is:
create or replace trigger DETRANSFORMMAP_VALID_TRIG
after insert or update on DE_TRANSFORM_MAP
declare
duplicate_rows_exist NUMBER;
begin
select 1 into duplicate_rows_exist from dual where exists (
select client, use_case, count(*) from de_transform_map where is_active = 1
group by client, use_case
having count(*) > 1
);
if duplicate_rows_exist = 1 then
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case may be active');
end if;
end;
The error you get means that you cannot query the table the trigger is on from within a row level trigger itself. One way to work around this problem is to use a combination of 3 triggers.
a) A before statement level trigger
b) A row level trigger
c) An after statement level trigger
Trigger A initializes a collection in a package
Trigger B adds every changed row to the collection
Trigger C performs the desired action for every entry in the collection.
More details here:
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
One of the improvements in Oracle 11G is that you can do all these action in one compound trigger. More here:
http://www.oracle-base.com/articles/11g/trigger-enhancements-11gr1.php
You should perhaps consider doing a "before insert" sort of thing! I've only got an MSSQL engine to play with right now, but hopefully something below might help you on your way... I'm not sure what you mean with your example of an error that works, however, as it appears to be in contradiction to the first use case you've posted... Either way, triggers can be a real pain during concurrent writes so you'll want to be careful in doing this sort of business logic validation from only the back end.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DE_TRANSFORM_MAP'
AND type = 'U' )
BEGIN
--DROP TABLE DE_TRANSFORM_MAP;
CREATE TABLE DE_TRANSFORM_MAP
(
DE_TRANSFORM_MAP_ID NUMERIC(10) NOT NULL,
PRIMARY KEY ( DE_TRANSFORM_MAP_ID ),
CLIENT NUMERIC( 5 ) NOT NULL,
USE_CASE NUMERIC( 38 ) NOT NULL,
DE_TRANSFORM_NAME NVARCHAR( 100 ) NOT NULL,
IS_ACTIVE TINYINT NOT NULL
);
END;
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DE_TRANSFORM'
AND type = 'U' )
BEGIN
--DROP TABLE DE_TRANSFORM;
CREATE TABLE DE_TRANSFORM
(
DE_TRANSFORM_ID NUMERIC( 10 ) NOT NULL,
PRIMARY KEY ( DE_TRANSFORM_ID ),
NAME NVARCHAR( 100 ) NOT NULL,
IS_ACTIVE TINYINT NOT NULL
);
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DETRANSFORMMAP_VALID_TRIG'
AND type = 'TR' )
BEGIN
--DROP TRIGGER DETRANSFORMMAP_VALID_TRIG;
EXEC( '
CREATE TRIGGER DETRANSFORMMAP_VALID_TRIG
ON DE_TRANSFORM_MAP INSTEAD OF INSERT, UPDATE
AS SET NOCOUNT OFF;' );
END;
GO
ALTER TRIGGER DETRANSFORMMAP_VALID_TRIG
ON DE_TRANSFORM_MAP INSTEAD OF INSERT, UPDATE
AS BEGIN
SET NOCOUNT ON;
IF ( ( SELECT MAX( IS_ACTIVE )
FROM ( SELECT IS_ACTIVE = SUM( IS_ACTIVE )
FROM ( SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM DE_TRANSFORM_MAP
EXCEPT
SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM DELETED
UNION ALL
SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM INSERTED ) f
GROUP BY CLIENT, USE_CASE ) mf ) > 1 )
BEGIN
RAISERROR( 'DE_TRANSFORM_MAP: CLIENT & USE_CASE cannot have multiple actives', 16, 1 );
END ELSE BEGIN
DELETE DE_TRANSFORM_MAP
WHERE DE_TRANSFORM_MAP_ID IN ( SELECT DE_TRANSFORM_MAP_ID
FROM DELETED );
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT DE_TRANSFORM_MAP_ID, CLIENT, USE_CASE,
DE_TRANSFORM_NAME, IS_ACTIVE
FROM INSERTED;
END;
SET NOCOUNT OFF;
END;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
VALUES ( 1, 6, 0, 'TEST', 1 );
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
VALUES ( 2, 6, 1, 'TEST', 1 );
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT 1, 6, 0, 'TEST', 1
UNION ALL SELECT 2, 6, 1, 'TEST', 1
UNION ALL SELECT 3, 6, 1, 'TEST2', 1;
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT 1, 6, 0, 'TEST', 1
UNION ALL SELECT 2, 6, 1, 'TEST', 0
UNION ALL SELECT 3, 6, 1, 'TEST2', 1;
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
UPDATE dbo.DE_TRANSFORM_MAP
SET IS_ACTIVE = 1
WHERE DE_TRANSFORM_MAP_ID = 2;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DETRANSFORM_VALID_TRIG'
AND type = 'TR' )
BEGIN
--DROP TRIGGER DETRANSFORM_VALID_TRIG;
EXEC( '
CREATE TRIGGER DETRANSFORM_VALID_TRIG
ON DE_TRANSFORM INSTEAD OF INSERT, UPDATE
AS SET NOCOUNT OFF;' );
END;
GO
ALTER TRIGGER DETRANSFORM_VALID_TRIG
ON DE_TRANSFORM INSTEAD OF INSERT, UPDATE
AS BEGIN
SET NOCOUNT ON;
IF ( ( SELECT MAX( IS_ACTIVE )
FROM ( SELECT IS_ACTIVE = SUM( IS_ACTIVE )
FROM ( SELECT NAME, IS_ACTIVE
FROM DE_TRANSFORM
EXCEPT
SELECT NAME, IS_ACTIVE
FROM DELETED
UNION ALL
SELECT NAME, IS_ACTIVE
FROM INSERTED ) f
GROUP BY NAME ) mf ) > 1 )
BEGIN
RAISERROR( 'DE_TRANSFORM: NAME cannot have multiple actives', 16, 1 );
END ELSE IF EXISTS (SELECT 1
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1
AND DE_TRANSFORM_NAME IN ( SELECT NAME
FROM DELETED
UNION ALL
SELECT NAME
FROM INSERTED
WHERE IS_ACTIVE = 0 ) )
BEGIN
RAISERROR( 'DE_TRANSFORM: NAME is active in DE_TRANSFORM_MAP', 16, 1 );
END ELSE BEGIN
DELETE DE_TRANSFORM
WHERE DE_TRANSFORM_ID IN (SELECT DE_TRANSFORM_ID
FROM DELETED );
INSERT INTO DE_TRANSFORM ( DE_TRANSFORM_ID, NAME, IS_ACTIVE )
SELECT DE_TRANSFORM_ID, NAME, IS_ACTIVE
FROM INSERTED;
END;
SET NOCOUNT OFF;
END;
GO
INSERT INTO DE_TRANSFORM ( DE_TRANSFORM_ID, NAME, IS_ACTIVE )
VALUES( 1, 'TEST2', 0 );
GO
SELECT *
FROM DE_TRANSFORM;
GO
TRUNCATE TABLE DE_TRANSFORM;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
If the trigger condition is “always” verified in the table, and if DE_TRANSFORM_MAP is a small table or if the insert/update statement affects many rows in DE_TRANSFORM_MAP, then you can use a statement trigger like this:
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG
AFTER INSERT OR UPDATE ON DE_TRANSFORM_MAP
DECLARE
EXISTS_ROWS NUMBER;
BEGIN
SELECT 1 INTO EXISTS_ROWS FROM DUAL WHERE EXISTS(
SELECT CLIENT
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1
GROUP BY CLIENT, USE_CASE
HAVING COUNT(*) > 1
);
IF (EXISTS_ROW = 1) THEN
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
END IF;
END;
/
If the trigger condition is “not always” verified in the table, and if DE_TRANSFORM_MAP is a big table or if the insert/update statement affects few rows in DE_TRANSFORM_MAP, then redesign your trigger following Rene's answer. Something like:
CREATE GLOBAL TEMPORARY TABLE DE_TRANSFORM_MAP_AUX AS
SELECT CLIENT, USE_CASE FROM DE_TRANSFORM_MAP WHERE 1 = 0;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG1
BEFORE INSERT OR UPDATE ON SERAPH.DE_TRANSFORM_MAP
BEGIN
DELETE FROM DE_TRANSFORM_MAP_AUX;
END;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG2
BEFORE INSERT OR UPDATE ON DE_TRANSFORM_MAP
FOR EACH ROW WHEN NEW.IS_ACTIVE = 1
BEGIN
INSERT INTO DE_TRANSFORM_MAP_AUX VALUES(:NEW.CLIENT, :NEW.USE_CASE);
END;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG3
AFTER INSERT OR UPDATE ON DE_TRANSFORM_MAP
DECLARE
EXISTS_ROW NUMBER;
BEGIN
SELECT 1 INTO EXISTS_ROWS FROM DUAL WHERE EXISTS(
SELECT CLIENT
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1 AND
(CLIENT, USE_CASE) IN (SELECT CLIENT, USE_CASE FROM DE_TRANSFORM_MAP_AUX)
GROUP BY CLIENT, USE_CASE
HAVING COUNT(*) > 1
);
DELETE FROM DE_TRANSFORM_MAP_AUX;
IF (EXISTS_ROW = 1) THEN
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
END IF;
END;
/
You must consider to create an index on CLIENT and USE_CASE in DE_TRANSFORM_MAP if it does not exists.