SQL trigger to prevent bit column from being updated from 1 to 0? - sql

Given a column with bit type in SQL Server 2008, how can I write a trigger to allow updates from 0 to 1, but disallow updating from 1 to 0?
In other words, once the bit is set to 1, it should always be 1.
The trigger must work for multiple updates, e.g. :
UPDATE Table SET BitField = 0
Should fail for any row where BitField = 1.
EDIT: To give some background, the bit/flag in question marks whether or not a monetary transaction needs to be processed. If the bit =1, the transaction has already been processed. If the bit is reset to 0, the transaction may be duplicated, so I need to enforce at the database level that the bit can not be reset to 0.
I need to protect against direct queries run against the database as well as application level bugs. I can't be sure that a stored procedure will always be used to update the table, so I believe a trigger is the only way to enforce this logic.

Looks like you are in need of a simple after trigger
CREATE TABLE YourTable(
PK int Primary key,
bitCol bit
)
CREATE TRIGGER YourTableTrigger
ON YourTable
AFTER UPDATE
AS
DECLARE #nrOfViolations int
select #nrOfViolations = count(*) from deleted d
join YourTable t on d.PK = t.PK
where d.bitCol = 1 and t.bitCol = 0
if #nrOfViolations > 0
BEGIN
RAISERROR('Failed', 16, 1);
ROLLBACK TRANSACTION
END

I wouldn't bury it in a Trigger. I'd have your stored procedure (SP) that updates the table do a check on the value. For example:
CREATE PROCEDURE dbo.proc_update_my_table
#id AS INT,
-- Whatever other params you need
#the_bit_field AS BIT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #existing_value AS BIT
SELECT #existing_value = the_bit_field FROM dbo.Table1 t WHERE t.id = #id
IF #existing_value = 1 AND #the_bit_field = 0
BEGIN
RAISERROR('Fail.', 10, 1)
RETURN -1
END
-- Update the table as normal.
END
GO
Using a Trigger is kind of like trying to get the thief (the error) out of the house after he got in through the open front door (the SP's query). Lock the front door instead ;)

One way would be an instead of trigger that lets all the updates through except anything that changes the bitfield once it's equal to 1. In that case it lets all the updates through except the bitfield change.
CREATE TRIGGER OneWayBitChange
ON YourTable
INSTEAD OF UPDATE
AS
BEGIN
UPDATE YourTable SET /* update all fields from original update except bitfield */
Field1 = i.Field1,
Field2 = i.Field2,
Field3 = i.Field3
FROM YourTable
INNER JOIN Inserted i ON i.PrimaryKey = YourTable.PrimaryKey
UPDATE YourTable SET /* update bitfield only if it's not already a 1 */
BitField = i.BitField
FROM YourTable
INNER JOIN Inserted i ON i.PrimaryKey = YourTable.PrimaryKey
WHERE IsNull(YourTable.BitField,0) < 1
END
GO
The above will allow all updates accept the one field when going from 1 to anything else (0, or null).
If you want to cancel any update on that row when an attempt is made on the bit field you can modify the body like this:
UPDATE YourTable SET /* update all except bitfield changes from 1 to 0 */
Field1 = i.Field1,
Field2 = i.Field2,
Field3 = i.Field3,
BitField = i.BitField
FROM YourTable
INNER JOIN Inserted i ON i.PrimaryKey = YourTable.PrimaryKey
WHERE IsNull(BitField,0) = 0 OR IsNull(i.BitField,0) = 1

Related

MSSQL Update: output value before update

There is a table with IDU (PK) and stat columns. If first bit of stat is 1 I need to set it to 0 and run some stored procedure in this case only, otherwise I do nothing.
Here is the simple query for this
DECLARE #s INT
-- get the current value of status before update
SET #s = (SELECT stat FROM myTable
WHERE IDU = 999999999)
-- check it first bit is 1
IF (#s & 1) = 1
BEGIN
-- first bit is 1, set it to 0
UPDATE myTable
SET status = Stat & ~1
WHERE IDU = 999999999
-- first bit is one, in this case we run our SP
EXEC SOME_STORED_PROCEDURE
END
But I'm not sure that this query is optimal. I heard about OUTPUT parameter for UPDATE query but I found how to get inserted value. Is there a way to get a value that was before insert?
Yes, OUTPUT clause allows you to get the previous value before the update. You need to look at deleted and inserted tables.
DELETED
Is a column prefix that specifies the value deleted by the
update or delete operation. Columns prefixed with DELETED reflect the
value before the UPDATE, DELETE, or MERGE statement is completed.
INSERTED
Is a column prefix that specifies the value added by the insert or
update operation. Columns prefixed with INSERTED reflect the value
after the UPDATE, INSERT, or MERGE statement is completed but before
triggers are executed.
-- Clear the first bit without checking what it was
DECLARE #Results TABLE (OldStat int, NewStat int);
UPDATE myTable
SET Stat = Stat & ~1
WHERE IDU = 999999999
OUTPUT
deleted.Stat AS OldStat
,inserted.Stat AS NewStat
INTO #Results
;
-- Copy data from #Results table into variables for comparison
-- Assumes that IDU is a primary key and #Results can have only one row
DECLARE #OldStat int;
DECLARE #NewStat int;
SELECT #OldStat = OldStat, #NewStat = NewStat
FROM #Results;
IF #OldStat <> #NewStat
BEGIN
EXEC SOME_STORED_PROCEDURE;
END;
Regardless of optimal, this query is not 100% safe. This is because between SET #s =... and UPDATE myTable there is no guarantee the value of stat has not been changed. If this code runs multiple times it is possible to screw up if two cases execute deadly close for the same IDU. The first thread will do ok but the second one will not, since the first would change the stat after the second read it and before update it. A select statement does not lock beyond its own execution time even on SERIALIZABLE isolation.
To be safe, you need to lock the record BEFORE read it, and to do that you need an update statement, even fake:
DECLARE #s INT
BEGIN TRANSACTION
UPDATE myTable SET stat = stat WHERE IDU = 999999999 --now you row lock your row, make sure no other thread can move along
-- get the current value of status before update
SET #s = (SELECT stat FROM myTable
WHERE IDU = 999999999)
-- check it first bit is 1
IF (#s & 1) = 1
BEGIN
-- first bit is 1, set it to 0
UPDATE myTable
SET status = Stat & ~1
WHERE IDU = 999999999
-- first bit is one, in this case we run our SP
-- COMMIT TRANSACTION here? depends on what SOME_STORED_PROCEDURE does
EXEC SOME_STORED_PROCEDURE
COMMIT TRANSACTION --i believe here you release the row lock
I am not sure what you mean by "Is there a way to get a value that was before insert" because you only update and the only data, stat, you had already read from the old record regardless if you update or not.
You could do this with an INSTEAD OF UPDATE Trigger.

SQL Server : update value if it does not exist in a column

I am using SQL Server 2008 and trying to create a statement which will update a single value within a row from another table if a certain parameter is met. I need to make this as simple as possible for a member of my team to use.
So in this case I want to store 2 values, the Sales Order and the reference. Unfortunately the Sales order has a unique identifier that I need to record and enter into the jobs table and NOT the actual sales order reference.
The parameter which needs to be met is that the Sales order unique identifier cannot exist anywhere in the sales order column within the jobs table. I can get this to work when the while value is set to 1 but not when it's set to 0 and in my head it should be set to 0. Anyone got any ideas why this doesn't work?
/****** Attach an SO to a WO ******/
/****** ONLY EDIT THE VALUES BETWEEN '' ******/
Declare #Reference nvarchar(30);
Set #Reference = 'WO16119';
Declare #SO nvarchar(30);
Set #SO = '0000016205';
/****** DO NOT ALTER THE CODE BEYOND THIS POINT!!!!!!!!!!!!! ******/
/* store more values */
Declare #SOID nvarchar(30);
Set #SOID = (Select SOPOrderReturnID
FROM Test_DB.dbo.SOTable
Where DocumentNo = #SO);
/* check if update should run */
Declare #Check nvarchar (30);
Set #Check = (Select case when exists (select *
from Test_DB.dbo.Jobs
where SalesOrderNumber != #SOID)
then CAST(1 AS BIT)
ELSE CAST(0 AS BIT) End)
While (#Check = 0)
/* if check is true run code below */
Begin
Update Test_DB.dbo.jobs
SET SalesOrderNumber = (Select SOPOrderReturnID
FROM Test_DB.dbo.SOPOrderReturn
Where DocumentNo = #SO)
Where Reference = #Reference
END;
A few comments. First in order to avoid getting into a never ending loop you may want to change your while for an IF statement. You aren't changing the #check value so that will run forever:
IF (#Check = 0)
BEGIN
/* if check is true run code below */
Update Test_DB.dbo.jobs
SET SalesOrderNumber = (Select SOPOrderReturnID
FROM Test_DB.dbo.SOPOrderReturn
Where DocumentNo = #SO)
Where Reference = #Reference
END
Then, without knowing your application I would say that the way you make checks is going to require you to lock your tables to avoid other users invalidating the results of your SELECTs.
I would go instead to creating a UNIQUE constraint over the column you want to be unique and handle the error gracefully. This way you don't need to create big locks on your tables
CREATE UNIQUE INDEX IX_UniqueIndex ON Test_DB.dbo.Jobs(SalesOrderNumber)
As per your comment if you cannot create a unique index you may want to try the following SQL although it could cause too much locking and be affected by race conditions:
IF NOT EXISTS (SELECT 1 FROM Test_DB.dbo.Jobs j INNER JOIN Test_DB.dbo.SOTable so ON j.SalesOrderNumber = so.SPOrderReturnId)
BEGIN
UPDATE Test_DB.dbo.jobs
SET SalesOrderNumber = so.SOPOrderReturnID
FROM
Test_DB.dbo.Jobs j
INNER JOIN Test_DB.dbo.SOTable so ON j.SalesOrderNumber = so.SPOrderReturnId
Where
Reference = #Reference
END
The risk of this are that you are running to separate queries (the select and the update) so between them the state of the database could change. So it may be possible that the first query returns nothing exists for that Id but at the moment of the update other user has inserted/updated that data so the previous result is no longer true.
You can try to avoid this problem by using a isolation level that locks the table on the read (like Serializable) but that could cause locks and even deadlocks in the database.
The best solution here is the unique index. If a certain column has to be unique inside a table the best controller system is the db itself by defining constraints.

Need to wait on specific column state change in SQL

I need to check for the status change of a certain column in the table
I can do it using while loop, where i can get the column value and check the value and break from the loop if value is changed.
i am using SQL server 2008.
is there a better way?
Here is the sample sql query
declare #status int = 1
select #status = status from MyTable with (nolock) where Id = 100034
while #status <> 3
begin
WAITFOR DELAY '00:01'
select #status = status from MyTable with (nolock) where Id = 100034
end
Have you considered to use a trigger instead of an stored procedure? This is exactly, what are triggers for.
CREATE TRIGGER reactOnStatus3
ON MyTable
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
IF Status = 3
EXEC DoTheMagicStoredProcedure;
END;

Creating a trigger to update multiple records after insert sql server 2008

Well basically I need this trigger to work after a user inserts multiple records into the database. So that when an optionID of 0 is inserted and the IsoptionalID = 1, then set the OptionID = NULL
CREATE TRIGGER ThisDatabase
ON OtherTable
AFTER INSERT
AS
BEGIN
DECLARE #OPTIONID INT
SET #OPTIONID = OtherTable.OPTIONID
DECLARE #ISoptional INT
SET #ISoptional = OtherTable.ISoptional
CASE #optionID WHEN 0 and #ISoptional = 1 set update OtherTable set optionid = null end
END
I am not sure about the case itself either.
Thank you in advance
This depends on the key field(s) of the table, but SQL Server triggers always work on the entire data set being modified (Inserted, Updated, or Deleted). So the trigger would something more like:
CREATE TRIGGER ThisDatabase
ON OtherTable
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
UPDATE ot
SET ot.OptionID = NULL
FROM OtherTable ot
INNER JOIN INSERTED ins
ON ins.KeyField = ot.KeyField
WHERE ins.OptionID = 0
AND ins.IsOptional = 1
END
The INSERTED table has the rows that were either Inserted or Updated (current version).
The DELETED table has the rows that were either Deleted or Updated (old version).
So, the INSERTED and DELETED tables are pre-filtered to only the changed records, but they are not updatable (since the event already happened due to this being an AFTER trigger and SQL Server not having a BEFORE trigger) so you need to do the UPDATE on the real table.
It isn't really clear what you want to do, but here's a skeleton. Just note:
Triggers are created on the table which is being affected (not an Other table)
You can certainly update another table as a consequence of a trigger. This is typically done through a join.
Use the inserted and deleted pseudo-tables to identify the record(s) which have been inserted, updated or deleted.
CREATE TRIGGER TR_TableBeingInsertedInto
ON TableBeingInsertedInto
AFTER INSERT
AS
BEGIN
UPDATE OtherTable
-- What you actually want to do here isn't clear to me
SET OtherTable.OPTIONID =
CASE i.OptionID
WHEN 0 THEN NULL
ELSE OtherTable.OPTIONID
END
FROM OtherTable
-- Inserted has the same schema as TableBeingInsertedInto
INNER JOIN INSERTED i
ON OtherTable.SomeCommonKey = i.SomeCommonKey;
END

IF UPDATE() in SQL server trigger

If there's:
IF UPDATE (col1)
...in the SQL server trigger on a table, does it return true only if col1 has been changed or been updated?
I have a regular update query like
UPDATE table-name
SET col1 = 'x',
col2 = 'y'
WHERE id = 999
Now what my concern is if the "col1" was 'x' previously then again we updated it to 'x'
would IF UPDATE ("col1") trigger return True or not?
I am facing this problem as my save query is generic for all columns, but when I add this condition it returns True even if it's not changed...So I am concerned what to do in this case if I want to add condition like that?
It returns true if a column was updated. An update means that the query has SET the value of the column. Whether the previous value was the same as the new value is largely irelevant.
UPDATE table SET col = col
it's an update.
UPDATE table SET col = 99
when the col already had value 99 also it's an update.
Within the trigger, you have access to two internal tables that may help. The 'inserted' table includes the new version of each affected row, The 'deleted' table includes the original version of each row. You can compare the values in these tables to see if your field value was actually changed.
Here's a quick way to scan the rows to see if ANY column changed before deciding to run the contents of a trigger. This can be useful for example when you want to write a history record, but you don't want to do it if nothing really changed.
We use this all the time in ETL importing processes where we may re-import data but if nothing really changed in the source file we don't want to create a new history record.
CREATE TRIGGER [dbo].[TR_my_table_create_history]
ON [dbo].[my_table] FOR UPDATE AS
BEGIN
--
-- Insert the old data row if any column data changed
--
INSERT INTO [my_table_history]
SELECT d.*
FROM deleted d
INNER JOIN inserted i ON i.[id] = d.[id]
--
-- Use INTERSECT to see if anything REALLY changed
--
WHERE NOT EXISTS( SELECT i.* INTERSECT SELECT d.* )
END
Note that this particular trigger assumes that your source table (the one triggering the trigger) and the history table have identical column layouts.
What you do is check for different values in the inserted and deleted tables rather than use updated() (Don't forget to account for nulls). Or you could stop doing unneeded updates.
Trigger:
CREATE TRIGGER boo ON status2 FOR UPDATE AS
IF UPDATE (id)
BEGIN
SELECT 'DETECT';
END;
Usage:
UPDATE status2 SET name = 'K' WHERE name= 'T' --no action
UPDATE status2 SET name = 'T' ,id= 8 WHERE name= 'K' --detect
To shortcut the "No actual update" case, you need also check at the beginning whether your query affected any rows at all:
set nocount on; -- this must be the first statement!
if not exists (select 1 from inserted) and not exists (select 1 from deleted)
return;
SET NOCOUNT ON;
declare #countTemp int
select #countTemp = Count (*) from (
select City,PostCode,Street,CountryId,Address1 from Deleted
union
select City,PostCode,Street,CountryId,Address1 from Inserted
) tempTable
IF ( #countTemp > 1 )
Begin
-- Your Code goes Here
End
-- if any of these "City,PostCode,Street,CountryId,Address1" got updated then trigger
-- will work in " IF ( #countTemp > 1 ) " Code)
This worked for me
DECLARE #LongDescDirty bit = 0
Declare #old varchar(4000) = (SELECT LongDescription from deleted)
Declare #new varchar(4000) = (SELECT LongDescription from inserted)
if (#old <> #new)
BEGIN
SET #LongDescDirty = 1
END
Update table
Set LongDescUpdated = #LongDescUpdated
.....