How to write this trigger for SQL Server? - sql

Suppose that I have a table my_table(id, x, y). I want to write a trigger to prevent updating the y col and setting it to a non-null value if x is already null. As SQL Server doesn't have a before update trigger, how can this be done? Apparently we can use an instead of trigger for this purpose, but how can we check the old and current values and decide whether we should raise an error or let the update execute normally?
Example:
Let's pretend we have this row in the DB:
1, null, null
Then this should fail (raise error)
update my_table set y = 'blah' where id = 1;
But this should succeed:
update my_table set y = null where id = 1;
I know the example isn't very meaningful, but it is similar to what I am trying to achieve.

This should work, but I am not sure what other edge conditions you need to handle:
create table my_table (id int identity, x varchar(20), y varchar(20))
go
CREATE TRIGGER tgNotNullYonMyTable
ON my_table
FOR UPDATE
AS
IF UPDATE(y)
BEGIN
IF exists (
select 1
from deleted d
join inserted i on i.id = d.id
where (d.x is null or i.x is null)
and i.y is null
)
BEGIN
RAISERROR ('Leave Y alone if x is null.', 16, 1)
rollback tran
END
END
go
insert my_table values (null,null)
go
update my_table set y = 'blah' where id = 1;
go
update my_table set y = null where id = 1;

CREATE TRIGGER yxnull
ON mytable
FOR UPDATE
AS
IF UPDATE(y)
BEGIN
IF deleted.x is null and inserted.y is not null
BEGIN
RAISERROR ('Leave Y alone if x is null.', 16, 1)
ROLLBACK TRANSACTION
END
END
go

Related

If Else Statement to Skip Value if populated

I am trying to create an if else script to skip a record if there is a specific value in a column of the table and continue to insert records into a different table.
How do I create the script to perform this action?
IF EXISTS (Select * From Table A where X =1)
BEGIN
Do nothing
END
ELSE
BEGIN
INSERT INTO TABLE Y
SELECT * FROM TABLE Z
END
Instead, write a single statement:
INSERT INTO TABLE Y
SELECT *
FROM TABLE Z
WHERE NOT EXISTS (Select 1 From Table A where X = 1);
The conditional is not needed at all.
If you want it in a proc to execute, use it this way
create procedure usp_insert
as
BEGIN
declare #rowcount int = (Select count(*) From TableA where X <>'1')
Begin
if #rowcount>=1
INSERT INTO TABLE Y
SELECT * FROM TABLE Z
end
END

How to identify the operation type(insert,update,delete) in SQL Server trigger

We are using the following trigger in SQL Server to maintain the history now I need to identify the operations just like insert,update or delete. I found some information HERE but it doesn't works with the SQL Server.
CREATE TRIGGER audit_guest_details ON [PMS].[GSDTLTBL]
FOR INSERT,UPDATE,DELETE
AS
DECLARE #SRLNUB1 INT;
DECLARE #UPDFLG1 DECIMAL(3,0);
SELECT #SRLNUB1 = I.SRLNUB FROM inserted I;
SELECT #UPDFLG1 = I.UPDFLG FROM inserted I;
BEGIN
/* Here I need to identify the operation and insert the operation type in the GUEST_ADT 3rd field */
insert into dbo.GUEST_ADT values(#SRLNUB1,#UPDFLG1,?);
PRINT 'BEFORE INSERT trigger fired.'
END;
GO
But here I need to identify the operation and want to insert operation type accordingly.
Here I don't want to create three trigger for every operations
For Inserted : Rows are in inserted only.
For Updated: Rows are in inserted and deleted.
For Deleted: Rows are in deleted only.
DECLARE #event_type varchar(42)
IF EXISTS(SELECT * FROM inserted)
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'update'
ELSE
SELECT #event_type = 'insert'
ELSE
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'delete'
ELSE
--no rows affected - cannot determine event
SELECT #event_type = 'unknown'
This is a simplified version of Mikhail's answer that uses a searched CASE expression.
DECLARE #Operation varchar(7) =
CASE WHEN EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS(SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE
NULL --Unknown
END;
Since you can get multiple rows at once we do it as follows.
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = CASE WHEN d.[PK_Field] IS NULL THEN 'I' ELSE 'U' END
,i.Field0
,i.Field1, d.Field1
FROM inserted i
LEFT JOIN deleted d on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NOT NULL
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = 'D'
,d.Field0
,d.Field1, NULL
FROM deleted d
LEFT JOIN inserted i on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NULL
create trigger my_trigger on my_table
after update , delete , insert
as
declare #inserting bit
declare #deleting bit
declare #updating bit = 0
select #inserting = coalesce (max(1),0) where exists (select 1 from inserted)
select #deleting = coalesce (max(1),0) where exists (select 1 from deleted )
select #inserting = 0
, #deleting = 0
, #updating = 1
where #inserting = 1 and #deleting = 1
print 'Inserting = ' + ltrim (#inserting)
+ ', Deleting = ' + ltrim (#deleting)
+ ', Updating = ' + ltrim (#updating)
If all three are zero, there are no rows affected and I think there is no way to tell whether it is an update/delete/insert.

Update or insert data depending on whether row exists

I have a collection of rows that I get from a web service. Some of these rows are to be inserted, some are updates to existing rows. There is no way of telling unless I do a query for the ID in the table. If I find it, then update. If I don't, then insert.
Select #ID from tbl1 where ID = #ID
IF ##ROWCOUNT = 0
BEGIN
Insert into tbl1
values(1, 'AAAA', 'BBBB', 'CCCC', 'DDD')
END
ELSE
BEGIN
UPDATE tbl1
SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID
END
I am trying to figure out the most effient way to update/insert these rows into the table without passing them into a stored procedure one at a time.
UPDATE 1
I should have mentioned I am using SQL Server 2005. Also if I have 300 records I don't want to make 300 stored procedure calls.
the most efficient way will be first try to update the table if it returns 0 row updated then only do insertion. for ex.
UPDATE tbl1
SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID
IF ##ROWCOUNT = 0
BEGIN
Insert into tbl1
values(1, 'AAAA', 'BBBB', 'CCCC', 'DDD')
END
ELSE
BEGIN
END
Instead of paying for a seek first and then updating using another seek, just go ahead and try to update. If the update doesn't find any rows, you've still only paid for one seek, and didn't have to raise an exception, but you know that you can insert.
UPDATE dbo.tbl1 SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.tbl1(ID,A,B,C,D)
VALUES(#ID,#AAA,#BBB,#CCC,#DDD);
END
You can also look at MERGE but I shy away from this because (a) the syntax is daunting and (b) there have been many bugs and several of them are still unresolved.
And of course instead of doing this one #ID at a time, you should use a table-valued parameter.
CREATE TYPE dbo.tbl1_type AS TABLE
(
ID INT UNIQUE,
A <datatype>,
B <datatype>,
C <datatype>,
D <datatype>
);
Now your stored procedure can look like this:
CREATE PROCEDURE dbo.tbl1_Update
#List AS dbo.tbl1_type READONLY
AS
BEGIN
SET NOCOUNT ON;
UPDATE t
SET A = i.A, B = i.B, C = i.C, D = i.D
FROM dbo.tbl1 AS t
INNER JOIN #List AS i
ON t.ID = i.ID;
INSERT dbo.tbl1
SELECT ID, A, B, C, D
FROM #List AS i
WHERE NOT EXISTS
(
SELECT 1
FROM dbo.tbl1 WHERE ID = i.ID
);
END
GO
Now you can just pass your DataTable or other collection from C# directly into the procedure as a single parameter.
From the collection of rows you get from the server find out which ones are already there:
select #id from tbl1 where id in (....)
Then you have a list of ids that are in the table and one that there are not in the table.
You will have then 2 batch operations: one for update, the other for insert.
what i understand is this :
at the front end u issue a single sql statement
ArrayofIDsforInsert = select ID from tbl1 where ID not in ( array of ids at the front end)
ArrayofIDsforUpdate = (IntialArrayofids at frontend) - (ArrayofIdsforInsert)
one insert into table and one update table...
now call the insert into table with ArrayofIds for insert
call the update table with ArrayofIds for update..

Update multiple rows in table from table variable

I'm writing a stored procedure to update multiple records based on a table variable parameter.
The existing table is: Tb_Project_Image with relevant columns:
id PK (identity 1,1)
cat_ord decimal(4,2)
The procedure will receive a temporary table variable (shown in the code below) containing the id as PI_ID, and the new value for cat_ord as newCatOrd. idx is a simple identity for each row containing 1...n where n is the rowcount of #tempTable.
For each row in #tempTable, I want to update Tb_Project_Image where id = PI_ID to the corresponding value.
DECLARE #tempTable table (
idx smallint Primary Key IDENTITY(1,1),
PI_ID bigint,
newCatOrd decimal(4, 2) not null )
INSERT INTO #tempTable values (3, 7.01)
INSERT INTO #tempTable values (4, 7.02)
INSERT INTO #tempTable values (5, 7.03)
--etc...
DECLARE #error int
DECLARE #update int
DECLARE #iter int
SET #iter = 1
BEGIN TRAN
WHILE #iter <= (select COUNT(*) from #tempTable)
BEGIN
UPDATE Tb_Project_Image
SET cat_ord = (SELECT newCatOrd FROM #tempTable
WHERE idx = #iter)
WHERE id = (SELECT PI_ID FROM #tempTable
WHERE idx = #iter)
--error checking
set #error = ##ERROR
set #update = ##ROWCOUNT
IF ((#error = 0) AND (#update = 1))
BEGIN
SET #iter = #iter + 1
CONTINUE
END
ELSE
BREAK
END
IF ((#error = 0) AND (#update = 1))
COMMIT TRAN
ELSE
ROLLBACK TRAN
GO
Now, the error checking is because, to ensure integrity, EACH row in the temporary table MUST make 1 update. (explanation omitted to save space) If a single iteration of the while loop threw an error, or didn't effect exactly 1 row, I want to break the loop and rollback the transaction
THE PROBLEM I'm having is that this error checking is not working. I'm currently running it with 14 rows in #tempTable and the 11th uses a PI_ID not found in the Project_Image table. Therefore, #update = 0... but it continues the loop and commits the data.
I'd be doubly glad if someone had a method of doing this that only used a single update statement.
You cannot do it this way, because even SET resets the state of ##ERROR and ##ROWNUMBER variables. In this case ##ROWCOUNT is set to 1 after set #error = ##ERROR. If you do not assign the values to local variables, your code will work:
IF ((##error = 0) AND (##rowcount = 1))
But you might rather try try...catch error handling and test ##rowcount separately after update.
UPDATE: doing it in single update:
UPDATE t
SET cat_ord = tt.newCatOrd
FROM Tb_Project_Image t
INNER JOIN #tempTable tt
ON t.id = tt.PI_ID
-- If there was PI_ID not found in Tb_Project_Image
-- But I think that this should have been dealt with
-- During the initial loading of temporary table
IF ##ROWCOUNT <> (select count (*) from #tempTable)
BEGIN
-- Error reporting here
ROLLBACK TRANSACTION
END
Instead of updating and then rolling back, you could also use a CTE to determine if any records should be updated prior to performing the update. Something like this should work:
WITH NON_SINGLETON AS (
-- Find any records in #tempTable that don't match
-- exactly one record in Tb_Project_Image
SELECT t.PI_ID, COUNT(pi.id) C
FROM #tempTable t
LEFT JOIN Tb_Project_Image pi ON t.PI_ID = pi.id
GROUP BY t.PI_ID
HAVING COUNT(pi.id) != 1
)
UPDATE Tb_Project_Image
SET cat_ord = t.newCatOrd
FROM Tb_Project_Image pi
JOIN #tempTable t ON pi.id = t.PI_ID
-- If any invalid records were found in the CTE,
-- then this condition will fail for all rows
-- and nothing will be updated
WHERE NOT EXISTS(SELECT 1 FROM NON_SINGLETON)
If it's possible for #tempTable to have duplicate entries for the same PI_ID, then this will handle those scenarios as well. And since it's a single statement, you don't have to explicitly managing the transaction in the proc (if it's the only thing that needs to be included in the transaction).

How do you reference new columns immediately after they are created in your SQL script

How do I need to write my SQL script to ensure my new column is visible on following lines after it is created.
This is the general form of my SQL:
BEGIN TRANSACTION
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
COMMIT
BEGIN TRANSACTION
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
BEGIN
UPDATE THIS_TABLE SET THIS_COLUMN = 1
END
COMMIT
This is the error I'm getting:
Invalid column name 'THIS_COLUMN'.
on this line:
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
The column has to be created before a query that uses it can be parsed. You can accomplish this by putting the update in a different batch, using the "go" keyword:
alter table t1 add c1 int
go
update t1 set c1 = 1
Or by running the second transaction as dynamic SQL:
alter table t1 add c1 int
exec ('update t1 set c1 = 1')
What Andomar said is correct, you need to use the go keyword.
However, the big problem is that your logic looks wrong. Let me go through each use case:
If THIS_TABLE is not empty
If the table is not empty, the if below returns false and you will never add the new column.
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
Then, the next script obviously fails, because there is no such column THIS_COLUMN:
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
If THIS_TABLE is empty
If the table is empty, the column is added:
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
But then the next if will always be true and the update statement will affect zero rows (because table is empty).
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
BEGIN
UPDATE THIS_TABLE SET THIS_COLUMN = 1
END