Write protect some rows in a table - sql-server

I have a situation where I need to write protect certain rows of a table based on some condition (actually a flag in a foreign table). Consider this schema:
CREATE TABLE Batch (
Id INT NOT NULL PRIMARY KEY,
DateCreated DATETIME NOT NULL,
Locked BIT NOT NULL
)
CREATE UNIQUE INDEX U_Batch_Locked ON Batch (Locked) WHERE Locked=0
CREATE TABLE ProtectedTable (
Id INT NOT NULL IDENTITY PRIMARY KEY,
Quantity DECIMAL(10,3) NOT NULL,
Price Money NOT NULL,
BatchId INT NULL)
ALTER TABLE ProtectedTable ADD CONSTRAINT FK_ProtectedTable_Batch FOREIGN KEY (BatchId) REFERENCES Batch(id)
I want to prevent changes to Quantity and Price if the row is linked to a Batch that has Locked=1. I also need to prevent the row from being deleted.
Note: U_Batch_Locked ensures that at most one batch can be unlocked at any time.
I have tried using triggers (yikes) but that caused more issues because the trigger rolls back the transaction. The update typically happens in a C# client that performs multiple updates (on multiple tables) in a single transaction. The client continues with the updates regardless of errors and at the end of the transaction rolls it back if any errors occurred. This way it can collect all/most of the constraint violations and let the user fix them before trying to save the changes again. However, since the trigger rolls back the transaction when the constraint is not satisfied, subsequent updates start their own auto transactions and are in fact committed. The final rollback issued by the client at the end of the batch simply fails.
I have also found this blog post: Using ROWVERSION to enforce business rules, which although seems to be what I need it requires the foreign key to have the opposite direction (i.e the protected table is the parent in the parent-child relationship while in my case the protected table is the child)
Has anyone done something like this before? It seems like a not so uncommon business requirement, yet I have not seen proper solution yet. I know I can implement this on the client side, but that leaves room for error: what if someone changes these using direct SQL (by mistake), what if an upgrade/migration script or the client itself has a bug and fails to enforce the constaint?

After lots of searching and trial and error, I ended up using INSTEAD OF triggers to check for the constraint and if necessary raise the error and skip the operation. Referring to the schema in the original question, here are the required triggers:
Update trigger:
CREATE TRIGGER ProtectedTable_LockU ON ProtectedTable
INSTEAD OF UPDATE
AS
SET NOCOUNT ON;
IF UPDATE(Quantity) OR UPDATE(Price)
AND EXISTS(
SELECT *
FROM inserted i INNER JOIN deleted d ON d.Id = i.Id
LEFT JOIN Batch b ON b.Id = d.BatchId
WHERE b.Locked <> 0 AND
(i.Quantity <> d. Quantity OR i.Price <> d.Price))
BEGIN
RAISERRROR('[CK_ProtectedTable_DataLocked]: Attempted to update locked data', 16, 1)
RETURN
END
UPDATE pt
SET Quantity = i.Quantity,
Price = i.Price,
BatchId = i.BatchId
FROM ProtectedTable pt
INNER JOIN deleted d ON d.Id = pt.ID
INNER JOIN inserted i ON i.Id = d.Id
Delete trigger:
CREATE TRIGGER ProtectedTable_LockD ON ProtectedTable
INSTEAD OF DELETE
AS
SET NOCOUNT ON;
IF EXISTS(
SELECT *
FROM deleted d
LEFT JOIN Batch b ON b.Id = d.BatchId
WHERE b.Locked <> 0)
BEGIN
RAISERRROR('[CK_ProtectedTable_DataLocked]: Attempted to delete locked data', 16, 1)
RETURN
END
DELETE pt
FROM ProtectedTable pt
INNER JOIN deleted d ON d.Id = pt.ID
Of course one could create a view and put the triggers on the view. That way you could also avoid the conflict between INSTEAD OF triggers and FOREIGN KEY ON DELETE/ON UPDATE rules.
It's ugly, I don't like it at all, but it's the only solution that actually works and behaves more or less like a regular database constraint (database constraints are checked before modifying the data). I have not yet tested this extensively and I am not sure there are no issues with it. I am also worried about race conditions (e.g. what if Batch is modified during the execution of the trigger? should I / can I include lock hints to ensure integrity?)
PS: Regardless of the bad application design, this is still a reasonable requirement. In certain cases it is also a legal one (you have to prove there is no way certain records can be altered after some conditions are met). That is why the question pops up now and then in SO, and there is no definite solution. I find this solution better than using AFTER triggers as this behaves more like a normal database constraint.
Hopefully this may help others in similar situations.

Rather than granting users access to the table directly, create a simple VIEW using the CHECK OPTION option and only grant users permissions to apply modifications via that view. Something like:
CREATE VIEW BatchData
WITH SCHEMABINDING
WITH CHECK OPTION
AS
SELECT pt.Id, pt.Quantity, pt.Price, pt.BatchId
FROM
dbo.ProtectedTable pt
INNER JOIN
dbo.Batch b
ON
pt.BatchId = b.Id
WHERE
b.Locked = 0
So long as all inserts, updates and deleted are channelled through this view (which as I say above, you constrain via permissions) then they can only be applied to the open batch

Related

Performance -- check for no rows or just perform *multiple* update statements?

I have a stored procedure that updates 2 separate tables based on 1 condition. In order to avoid a race condition, I am first recording the primary key of the rows that need to be updated in a temp table and then updating that table plus another table (foreign key reference) from that temp table.
Over 99% of the time that this is run there will be 0 rows to update. Thus from a performance perspective I'd like to know if it would be worth checking for any rows before performing updates on no rows.
select distinct matters
into #matters_t
from mattersqdefaultprocess
where reopen is not null
and reopen = 'Y'
update m
set status = 'Open'
from matters m
inner join #matters_t mu on mu.matters = m.matters
where m.status in ('inactive', 'closed')
update mdp
set reopen = 'N'
from mattersqdefaultprocess mdp
inner join #matters_t mu on mu.matters = mdp.matters
It seems logical to me that it would be beneficial to check for no rows if there are, say 50 update statements. For only 2? That is the question, I suppose.
Perhaps I should just do this:
if 0 < (select count(*)
from mattersqdefaultprocess mdp
where qdefprocreopen is not null
and qdefprocreopen = 'Y')
begin
--do the temp table seeding and updates here
end
In the general absence of any answers or discussion, I went ahead and set it up with the if statement and everything (including creating temp tables) else inside the true part of the if statement.

Triggers Inner join inserted with orginial table

I was reviewing creating DML triggers in SQL Server in SQL docs: Use the inserted and deleted Tables
There is an example which do the following:
The following example creates a DML trigger. This trigger checks to make sure the credit rating for the vendor is good when an attempt is made to insert a new purchase order into the PurchaseOrderHeader table. To obtain the credit rating of the vendor corresponding to the purchase order that was just inserted, the Vendor table must be referenced and joined with the inserted table. If the credit rating is too low, a message is displayed and the insertion does not execute.
Note that this example does not allow for multirow data modifications.
USE AdventureWorks2012;
GO
IF OBJECT_ID ('Purchasing.LowCredit','TR') IS NOT NULL
DROP TRIGGER Purchasing.LowCredit;
GO
-- This trigger prevents a row from being inserted in the Purchasing.PurchaseOrderHeader table
-- when the credit rating of the specified vendor is set to 5 (below average).
CREATE TRIGGER Purchasing.LowCredit
ON Purchasing.PurchaseOrderHeader
AFTER INSERT
AS
IF EXISTS (SELECT *
FROM Purchasing.PurchaseOrderHeader p
JOIN inserted AS i ON p.PurchaseOrderID = i.PurchaseOrderID
JOIN Purchasing.Vendor AS v ON v.BusinessEntityID = p.VendorID
WHERE v.CreditRating = 5)
BEGIN
RAISERROR ('A vendor''s credit rating is too low to accept new purchase orders.', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
GO
I wonder why the example inner joined the inserted table with Purchasing.PurchaseOrderHeader table and then join the vendor table.
Can I get the same result using only the inserted table joining the vendor table directly without joining with Purchasing.PurchaseOrderHeader table?
Not only do I believe you are correct, I think there are further inaccuracies on this documentation page - which I have to admit is pretty rare in my experience.
Take for example this part:
Note that this example does not allow for multirow data modifications.
This is a false claim. The trigger code example will handle multiple rows insert as well as a single row insert.
Note that according to the documented (and observed) behavior, the inserted table will contain all the rows inserted (or updated) to the trigger's target table:
The inserted table stores copies of the affected rows during INSERT and UPDATE statements. During an insert or update transaction, new rows are added to both the inserted table and the trigger table. The rows in the inserted table are copies of the new rows in the trigger table.
Therefor, the join to inserted should be enough in this case to enforce the business rule discussed.
That being said, using triggers to enforce business rules might prove difficult and even problematic - note that this trigger only covers inserted rows, but not updated rows. This means that a new row might be inserted with valid values, and later on updated to invalid values.

Create Trigger to modify a row to the value on a joined table

I have a tables job_costcodes(id, cost_code_no, dept_id) and cost_codes(code_code_no, dept_id).
I am trying to make it so if job_costcodes.cost_code_no is modified, job_costcodes.dept_id is filled with the appropriate one from the cost_codes table, based on a matching code_code_no.
So referring to the tables below, if the top row in job_costcodes is changed to 10, the dept_id should change to 1212. Or 20 to 1313, etc.
I am not sure exactly how the syntax works... here is what I have so far.
UPDATE: updated code.. i think it works now.
create trigger update_test on dbo.job_costcodes
for update, insert
as
begin
set nocount on
update dbo.job_costcodes
set dept_id = (select CASE WHEN COUNT(1) > 0 THEN MIN(dbo.cost_codes.dept_id) ELSE NULL END as Expr1
FROM inserted INNER JOIN
dbo.cost_codes ON dbo.cost_codes.cost_code_no = inserted.cost_code_no)
from inserted as i
inner join dbo.[job_costcodes] on dbo.[job_costcodes].id = i.id
end
Treating your question as academic, start off by looking up the CREATE TRIGGER command in TSQL to get a solid understanding of the virtual tables inserted and deleted.
Then here is what I would do, in pseudo-cod-ish descriptive terms:
In your trigger, simply UPDATE job_costcodes and set the value of dept_id to the corresponding dept_id in cost_codes by JOINing to cost_codes and inserted in the FROM clause of the UPDATE.
There is no need to verify that the cost_code_no changed when doing this, the result will be the same, but if you feel you must do this, then look at the IF UPDATE() function in TSQL. You can then compare the value of cost_code_no in inserted vs deleted to know if it changed at all.

Finding out which rows are inserted , updated or deleted using triggers

I have a table named indication in Database and it has three columns Name, Age, and Enable.
I want to create a trigger that inserts an alarm into my alarm table whenever the Age is under 18 and Enable is true, I want to check the record on the indication table at the exact moment that it has been inserted, that way I can check whether it should be inserted in alarm or not.
I found COLUMNS_UPDATED (Transact-SQL) on MSDN and it works for updated columns, is there the same thing for ROWS_UPDATED?
You can always set your trigger to respond to only an INSERT action, with
CREATE TRIGGER TR_Whatever_I ON dbo.YourTable FOR INSERT
AS
... (body of trigger)
Be aware FOR INSERT is the same as AFTER INSERT. You also have the option of INSTEAD OF, but with that you have to perform the data modification yourself. There is no BEFORE trigger in SQL Server.
In some cases it is very convenient to handle more than one action at once because the script for the different actions is similar--why write three triggers when you can write just one? So in the case where your trigger looks more like this:
CREATE TRIGGER TR_Whatever_IUD ON dbo.YourTable FOR INSERT, UPDATE, DELETE
AS
... (body of trigger)
Then you don't automatically know it was an insert in the body. In this case, you can detect whether it's an insert similar to this:
IF EXISTS (SELECT * FROM Inserted)
AND NOT EXISTS (SELECT * FROM Deleted) BEGIN
--It's an INSERT.
END
Or, if you want to determine which of the three DML operations it is:
DECLARE #DataOperation char(1);
SET #DataOperation =
CASE
WHEN NOT EXISTS (SELECT * FROM Inserted) THEN 'D'
WHEN NOT EXISTS (SELECT * FROM Deleted) THEN 'I'
ELSE 'U'
END
;
Triggers still run if a DML operation affects no rows (for example, UPDATE dbo.YourTable SET Column = '' WHERE 1 = 0). In this case, you can't tell whether it was an update, delete, or insert--but since no modification occurred, it doesn't matter.
A Special Note
It's worth mentioning that in SQL Server, triggers fire once per operation, NOT once per row. This means that the Inserted and Deleted meta-tables will have as many rows in them during trigger execution as there are rows affected by the operation. Be careful and don't write triggers that assume there will only be one row.
Firstly I think you have to increase your knowledge on the way triggers work, and what the different type of triggers are.
You can create a trigger like this
CREATE TRIGGER trg_Indication_Ins
ON Indication
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
Insert Alarms (column1, column2) Select value1, value2 from inserted where Age < 18 and Enable = 1
END
This should basically do what you are looking for, and from what I understand from your quesion.
UPDATE:
Basically triggers can fire on INSERT, UPDATE or DELETE or any combination of the three, You can also set it to fire 'FOR/AFTER' the event (of which both actually means AFTER), or INSTEAD OF the event. A trigger will always have "internal" or meta-tables on the event.
These tables are inserted and deleted
The inserted table is basically all the new records that is applied to the table, and the deleted table have all the records that will be removed. In the case of the UPDATE event, the inserted table will have all the new values and deleted will have all the old values.
The inserted table will be empty on a DELETE trigger, and the deleted table will be empty on an INSERT trigger
Triggers can affect performance drastically if not used properly, so use it wisely.

SQL Server Trigger loop

I would like to know if there is anyway I can add a trigger on two tables that will replicate the data to the other.
For example:
I have a two users tables, users_V1 and users_V2, When a user is updated with one of the V1 app, it activate a trigger updating it in users_V2 as well.
If I want to add the same trigger on the V2 table in order to update the data in V1 when a user is updated in V2, will it go into an infinite loop? Is there any way to avoid that.
I don't recommend explicitly disabling the trigger during processing - this can cause strange side-effects.
The most reliable way to detect (and prevent) cycles in a trigger is to use CONTEXT_INFO().
Example:
CREATE TRIGGER tr_Table1_Update
ON Table1
FOR UPDATE AS
DECLARE #ctx VARBINARY(128)
SELECT #ctx = CONTEXT_INFO()
IF #ctx = 0xFF
RETURN
SET #ctx = 0xFF
-- Trigger logic goes here
See this link for a more detailed example.
Note on CONTEXT_INFO() in SQL Server 2000:
Context info is supported but apparently the CONTEXT_INFO function is not. You have to use this instead:
SELECT #ctx = context_info
FROM master.dbo.sysprocesses
WHERE spid = ##SPID
Either use TRIGGER_NESTLEVEL() to restrict trigger recursion, or
check the target table whether an UPDATE is necessary at all:
IF (SELECT COUNT(1)
FROM users_V1
INNER JOIN inserted ON users_V1.ID = inserted.ID
WHERE users_V1.field1 <> inserted.field1
OR users_V1.field2 <> inserted.field2) > 0 BEGIN
UPDATE users_V1 SET ...
I had the exact same problem. I tried using CONTEXT_INFO() but that is a session variable and so it works only the first time! Then next time a trigger fires during the session, this won't work. So I ended up with using a variable that returns Nest Level in each of the affected triggers to exit.
Example:
CREATE TRIGGER tr_Table1_Update
ON Table1
FOR UPDATE AS
BEGIN
--Prevents Second Nested Call
IF ##NESTLEVEL>1 RETURN
--Trigger logic goes here
END
Note: Or use ##NESTLEVEL>0 if you want to stop all nested calls
One other note -- There seems to be much confusion in this article about nested calls and recursive calls. The original poster was referring to a nested trigger where one trigger would cause another trigger to fire, which would cause the first trigger to fire again, and so on. This is Nested, but according to SQL Server, not recursive because the trigger is not calling/triggering itself directly. Recursion is NOT where "one trigger [is] calling another". That is nested, but not necessarily recursive. You can test this by enabling/disabling recursion and nesting with some settings mentioned here: blog post on nesting
I'm with the no triggers camp for this particular design scenario. Having said that, with the limited knowledge I have about what your app does and why it does it, here's my overall analysis:
Using a trigger on a table has an advantage of being able to act on all actions on the table. That's it, your main benefit in this case. But that would mean you have users with direct access to the table or multiple access points to the table. I tend to avoid that. Triggers have their place (I use them a lot), but it's one of the last database design tools I use because they tend to not know a lot about their context (generally, a strength) and when used in a place where they do need to know about different contexts and overall use cases, their benefits are weakened.
If both app versions need to trigger the same action, they should both call the same stored proc. The stored proc can ensure that all the appropriate work is done, and when your app no longer needs to support V1, then that part of the stored proc can be removed.
Calling two stored procs in your client code is a bad idea, because this is an abstraction layer of data services which the database can provide easily and consistently, without your application being worried about it.
I prefer to control the interface to the underlying tables more - with either views or UDFs or SPs. Users never get direct access to a table. Another point here is that you could present a single "users" VIEW or UDF coalescing the appropriate underlying tables without the user even knowing about - perhaps getting to the point where there is not even any "synchronization" necessary, since new attributes are in an EAV system if you need that kind of pathological flexibility or in some other different structure which can still be joined - say OUTER APPLY UDF etc.
You're going to have to create some sort of loopback detection within your trigger. Perhaps using an "if exists" statement to see if the record exists before entering it into the next table. It does sound like it will go into an infinite loop the way it's currently set up.
Avoid triggers like the plague .... use a stored procedure to add the user. If this requires some design changes then make them. Triggers are the EVIL.
Try something like (I didn;t bother with thecreate trigger stuff as you clearly already know how to write that part):
update t
set field1 = i.field1
field2 = i.field2
from inserted i
join table1 t on i.id = t.id
where field1 <> i.field1 OR field2 <> i.field2
Recursion in triggers, that is, one trigger calling another, is limited to 32 levels
In each trigger, just check if the row you wish to insert already exists.
Example
CREATE TRIGGER Table1_Synchronize_Update ON [Table1] FOR UPDATE AS
BEGIN
UPDATE Table2
SET LastName = i.LastName
, FirstName = i.FirstName
, ... -- Every relevant field that needs to stay in sync
FROM Table2 t2
INNER JOIN Inserted i ON i.UserID = t2.UserID
WHERE i.LastName <> t2.LastName
OR i.FirstName <> t2.FirstName
OR ... -- Every relevant field that needs to stay in sync
END
CREATE TRIGGER Table1_Synchronize_Insert ON [Table1] FOR INSERT AS
BEGIN
INSERT INTO Table2
SELECT i.*
FROM Inserted i
LEFT OUTER JOIN Table2 t2 ON t2.UserID = i.UserID
WHERE t2.UserID IS NULL
END
CREATE TRIGGER Table2_Synchronize_Update ON [Table2] FOR UPDATE AS
BEGIN
UPDATE Table1
SET LastName = i.LastName
, FirstName = i.FirstName
, ... -- Every relevant field that needs to stay in sync
FROM Table1 t1
INNER JOIN Inserted i ON i.UserID = t1.UserID
WHERE i.LastName <> t1.LastName
OR i.FirstName <> t1.FirstName
OR ... -- Every relevant field that needs to stay in sync
END
CREATE TRIGGER Table2_Synchronize_Insert ON [Table2] FOR INSERT AS
BEGIN
INSERT INTO Table1
SELECT i.*
FROM Inserted i
LEFT OUTER JOIN Table1 t1 ON t1.UserID = i.UserID
WHERE t1.UserID IS NULL
END

Resources