views:

44453

answers:

12

Consider this trigger:

ALTER TRIGGER myTrigger 
   ON someTable 
   AFTER INSERT
AS BEGIN
  DELETE FROM someTable
         WHERE ISNUMERIC(someField) = 1
END

I've got a table, someTable, and I'm trying to prevent people from inserting bad records. For the purpose of this question, a bad record has a field "someField" that is all numeric.

Of course, the right way to do this is NOT with a trigger, but I don't control the source code... just the SQL database. So I can't really prevent the insertion of the bad row, but I can delete it right away, which is good enough for my needs.

The trigger works, with one problem... when it fires, it never seems to delete the just-inserted bad record... it deletes any OLD bad records, but it doesn't delete the just-inserted bad record. So there's often one bad record floating around that isn't deleted until somebody else comes along and does another INSERT.

Is this a problem in my understanding of triggers? Are newly-inserted rows not yet committed while the trigger is running?

+23  A: 

Triggers cannot modify the changed data (inserted or deleted) otherwise you could get infinite recursion as the changes invoked the trigger again. One option would be for the trigger to roll back the transaction.

Edit: The reason for this is that the standard for SQL is that inserted and deleted rows cannot be modified by the trigger. The underlying reason for is that the modifications could cause infinite recursion. In the general case, this evaluation could involve multiple triggers in a mutually recursive cascade. Having a system intelligently decide whether to allow such updates is computationally intractable, essentially a variation on the halting problem.

The accepted solution to this is not to permit the trigger to alter the changing data, although it can roll back the transaction.

ConcernedOfTunbridgeWells
Um...so long as you're not doing an insert on or after insert you wouldn't trigger infinite recursion. Here Joel's trying to delete on insert which wouldn't cause the trigger to refire.
BenAlabaster
My recollection of the theory is a bit hazy but IIRC actually trying to decide that in the general case is intractable. You could wind up with mutually recursive triggers or other things that can't be statically evaluated. The 'standard' approach is not to support updating the changing data.
ConcernedOfTunbridgeWells
MSSQL allows nested and recursive triggers. http://msdn.microsoft.com/en-us/library/aa258254(SQL.80).aspx
Mark Brackett
Recursive triggers as discussed in the MSDN article aren't quite the same thing. If you update 'inserted' or 'deleted' it is computationally intractable to decide what should be seen by other triggers. Imagine if two or three or 10 triggers made updates - what is the correct data? What if the updates (or even whwther an update is made) are contingent on the contents of the rows? Now the correct answer requires the decision mechanism to interpret the triggers. Open Wikipedia and read about the undecidability proof of the Halting Problem.
ConcernedOfTunbridgeWells
+10  A: 

I think you can use CHECK constraint - it is exactly what it was invented for.

ALTER TABLE someTable 
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;

My previous answer (also right by may be a bit overkill):

I think the right way is to use INSTEAD OF trigger to prevent the wrong data from being inserted (rather than deleting it post-factum)

Dmitry Khalatov
what would that look like?
Joel Spolsky
See my separate answer. +1 to Dmitry.
Bill Karwin
It turns out the reason we were using a trigger is that the running code didn't handle insert failures well. In most cases, you're right, we shouldn't use a trigger. But the original question still might be relevant to someone, so I'm accepting the answer that addresses it exactly.
Joel Spolsky
+3  A: 

I found this reference:

create trigger myTrigger
on SomeTable
for insert 
as 
if (select count(*) 
    from SomeTable, inserted 
    where IsNumeric(SomeField) = 1) <> 0
/* Cancel the insert and print a message.*/
  begin
    rollback transaction 
    print "You can't do that!"  
  end  
/* Otherwise, allow it. */
else
  print "Added successfully."

I haven't tested it, but logically it looks like it should dp what you're after...rather than deleting the inserted data, prevent the insertion completely, thus not requiring you to have to undo the insert. It should perform better and should therefore ultimately handle a higher load with more ease.

Edit: Of course, there is the potential that if the insert happened inside of an otherwise valid transaction that the wole transaction could be rolled back so you would need to take that scenario into account and determine if the insertion of an invalid data row would constitute a completely invalid transaction...

BenAlabaster
Couldn't this rule potentially rollback a valid transaction? Imagine this scenario. 1) Update a row to have a bad numeric 2) then insert valid data. Wouldn't the trigger prevent #2 from ever suceeding?
JaredPar
I guess theoretically you're right. I'll add an addendum to qualify that.
BenAlabaster
+2  A: 

From the CREATE TRIGGER documentation:

deleted and inserted are logical (conceptual) tables. They are structurally similar to the table on which the trigger is defined, that is, the table on which the user action is attempted, and hold the old values or new values of the rows that may be changed by the user action. For example, to retrieve all values in the deleted table, use: SELECT * FROM deleted

So that at least gives you a way of seeing the new data.

I can't see anything in the docs which specifies that you won't see the inserted data when querying the normal table though...

Jon Skeet
A: 

The techniques outlined above describe your options pretty well. But what are the users seeing? I can't imagine how a basic conflict like this between you and whoever is responsible for the software can't end up in confusion and antagonism with the users.

I'd do everything I could to find some other way out of the impasse - because other people could easily see any change you make as escalating the problem.

EDIT:

I'll score my first "undelete" and admit to posting the above when this question first appeared. I of course chickened out when I saw that it was from JOEL SPOLSKY. But it looks like it landed somewhere near. Don't need votes, but I'll put it on the record.

IME, triggers are so seldom the right answer for anything other than fine-grained integrity constraints outside the realm of business rules.

le dorfier
It's for a spam filter. The spamming process needs to see the record getting inserted successfully, otherwise the spammer will just modify their spam techniques until it does. We don't want the spammer to know there's a REGEXP that catches his spam or he'll work around it.
Joel Spolsky
So - a legitimate case for an intentionally hostile user interface. Good one.
le dorfier
+22  A: 

You can reverse the logic. Instead of deleting an invalid row after it has been inserted, write an INSTEAD OF trigger to insert only if you verify the row is valid.

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  DECLARE @isnum TINYINT;

  SELECT @isnum = ISNUMERIC(somefield) FROM inserted;

  IF (@isnum = 1)
    INSERT INTO sometable SELECT * FROM inserted;
  ELSE
    RAISERROR('somefield must be numeric', 16, 1)
      WITH SETERROR;
END

If your application doesn't want to handle errors (as Joel says is the case in his app), then don't RAISERROR. Just make the trigger silently not do an insert that isn't valid.

I ran this on SQL Server Express 2005 and it works. Note that INSTEAD OF triggers do not cause recursion if you insert into the same table for which the trigger is defined.

Bill Karwin
Thanks, but I found that CHECK constraint may be even better solution
Dmitry Khalatov
Yes, CHECK constraint is also good. I assume Joel simplified the nature of his condition, and some complex conditions may not be best implemented in a CHECK constraint.
Bill Karwin
Right. It's important to us that the insert NOT fail, because that would crash the process doing the insert.
Joel Spolsky
It's a tricky problem, because you're setting up a condition where the app *thinks* it performed an insert, but it didn't happen. Could lead to data integrity problems.
Bill Karwin
If you don't want the insert to fail, use Bill's code, but instead of doing an insert into sometable, insert into a DIFFERENT error logging table, like sometableRejects. Let the insert continue, and the app won't know which table it actually inserted into.
Brent Ozar
Bill your solution is a bad practice ina trigger. It will only work if one record is inserted. All triggers must be designed to handle multiple record inserts.
HLGEM
@HLGEM: Yep, you're right. I've since learned that MS SQL Server triggers fire once for all rows in a given operation. I've done most of my work with InterBase/Firebird and MySQL, in which triggers fire once for each row modified.
Bill Karwin
A: 

Is it possible the INSERT is valid, but that a separate UPDATE is done afterwards that is invalid but wouldn't fire the trigger?

SqlACID
+15  A: 

Here's my modified version of Bill's code:

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 1 FROM inserted;
  INSERT INTO sometableRejects SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 0 FROM inserted;
END

This lets the insert always succeed, and any bogus records get thrown into your sometableRejects where you can handle them later. It's important to make your rejects table use nvarchar fields for everything - not ints, tinyints, etc - because if they're getting rejected, it's because the data isn't what you expected it to be.

This also solves the multiple-record insert problem, which will cause Bill's trigger to fail. If you insert ten records simultaneously (like if you do a select-insert-into) and just one of them is bogus, Bill's trigger would have flagged all of them as bad. This handles any number of good and bad records.

I used this trick on a data warehousing project where the inserting application had no idea whether the business logic was any good, and we did the business logic in triggers instead. Truly nasty for performance, but if you can't let the insert fail, it does work.

Brent Ozar
+4  A: 

UPDATE: DELETE from a trigger works on both MSSql 7 and MSSql 2008.

I'm no relational guru, nor a SQL standards wonk. However - contrary to the accepted answer - MSSQL deals just fine with both recursive and nested trigger evaluation. I don't know about other RDBMSs.

The relevant options are 'recursive triggers' and 'nested triggers'. Nested triggers are limited to 32 levels, and default to 1. Recursive triggers are off by default, and there's no talk of a limit - but frankly, I've never turned them on, so I don't know what happens with the inevitable stack overflow. I suspect MSSQL would just kill your spid (or there is a recursive limit).

Of course, that just shows that the accepted answer has the wrong reason, not that it's incorrect. However, prior to INSTEAD OF triggers, I recall writing ON INSERT triggers that would merrily UPDATE the just inserted rows. This all worked fine, and as expected.

A quick test of DELETEing the just inserted row also works:

 CREATE TABLE Test ( Id int IDENTITY(1,1), Column1 varchar(10) )
 GO

 CREATE TRIGGER trTest ON Test 
 FOR INSERT 
 AS
    SET NOCOUNT ON
    DELETE FROM Test WHERE Column1 = 'ABCDEF'
 GO

 INSERT INTO Test (Column1) VALUES ('ABCDEF')
 --SCOPE_IDENTITY() should be the same, but doesn't exist in SQL 7
 PRINT @@IDENTITY --Will print 1. Run it again, and it'll print 2, 3, etc.
 GO

 SELECT * FROM Test --No rows
 GO

You have something else going on here.

Mark Brackett
A: 

MS-SQL has a setting to prevent recursive trigger firing. This is confirgured via the sp_configure stored proceedure, where you can turn recursive or nested triggers on or off.

In this case, it would be possible, if you turn off recursive triggers to link the record from the inserted table via the primary key, and make changes to the record.

In the specific case in the question, it is not really a problem, because the result is to delete the record, which won't refire this particular trigger, but in general that could be a valid approach. We implemented optimistic concurrency this way.

The code for your trigger that could be used in this way would be:

ALTER TRIGGER myTrigger
    ON someTable
    AFTER INSERT
AS BEGIN
DELETE FROM someTable
    INNER JOIN inserted on inserted.primarykey = someTable.primarykey
    WHERE ISNUMERIC(inserted.someField) = 1
END
Yishai
A: 

IF i am using update and delete in trigger. how would i know that row is deleted or updated in the same trigger and how to store individually.

dd
A: 

can it possible that we van create a trigger on view to update the view.

vaishali