SQL Server 2008 Trigger Errors - sql

I need some help with a database trigger that I just can't get working. Disclosure : I'm not a DBA and don't claim any expertise, but I've been tasked with getting this done..
That said, here is the problem statement:
I have a database with a few tables that we want to track updates/inserts/deletes on. The trigger should catch the "before" and the "after" values of some columns, take both along with some additional information (mentioned below) and add them to a versioncontrol table.
I've put together what I think should work based on tutorial and other information available on various sites, but no matter what I do when I try to create the trigger it always throws an error:
Msg 311, Level 16, State 1, Procedure tr_taskconstructs_U, Line 38
Cannot use text, ntext, or image columns in the 'inserted' and 'deleted' tables.
The column layout for the table I'm applying the trigger to is very simple:
ResourceID (PK, nvarchar(38))
AML (ntext, null)
I want to catch changes to the AML column and insert the changes into a table, as follows:
id (int, not null),
ResourceID (nvarchar(38)),
TableName (nvarchar(50)),
DateTime (datetime),
Type (varchar(20)),
Before (ntext),
After (ntext)
My code is as follows:
CREATE TRIGGER tr_taskconstructs_U ON taskconstructs AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #before nvarchar(max),
#after nvarchar(max),
#resource nvarchar(50)
IF UPDATE(AML)
BEGIN
SELECT #before = d.AML,
#after = i.AML,
#resource = d.ResourceID
FROM inserted i
INNER JOIN deleted d on d.ResourceID = i.ResourceID
INSERT INTO versioncontrol
VALUES (#resource, 'taskconstructs', GetDate(), 'Update', #before, #after)
END
END
GO
It always fails with the error mentioned above. I tried changing the datatypes of the variables, etc, with the same result. I've tried commenting out almost everything and it also has the same error. I'm obviously missing something, but am not able to figure out what.
Any help would be appreciated.

change the table to use the nvarchar(max) datatype instead. Ntext is deprecated anyhow and should not be used going forward

Like said in the other answer ntext, along with text and image are deprecated datatypes which triggers can't really work with. They will be removed for future versions of SQL Server so your best solution would be to change them to NVARCHAR(MAX), VARCHAR(MAX) or VARBINARY(MAX) respectively.
http://msdn.microsoft.com/en-us/library/ms189799.aspx
However, if that's not a possibility for you at the moment - INSTEAD OF trigger can work with them so you can try to intercept the updates and do them from trigger, filling your log table in the process.
But, before I show you example of that, I must point out a big mistake you have in your existing trigger - You have wrote the query that works only on single row from INSERTED which is not good as UPDATE can often update more then 1 row at the time. Even if your app is not designed to allow it, you should never write trigger asuming it is always going to be single row.
So your trigger should have looked like this:
(and should look like this if/after you change that column)
CREATE TRIGGER tr_taskconstructs_U ON taskconstructs AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF UPDATE(AML)
BEGIN
INSERT INTO versioncontrol
SELECT ResourceID, 'taskconstructs', GetDate(), 'Update'. d.AML, i.AML
FROM inserted i
INNER JOIN deleted d on d.ResourceID = i.ResourceID
END
END
GO
Now, for the INSTEAD OF TRIGGER:
CREATE TRIGGER tr_taskconstructs_U ON taskconstructs INSTEAD OF UPDATE
AS
BEGIN
--insert log
INSERT INTO versioncontrol
SELECT i.ResourceID, 'taskconstructs', GetDate(), 'Update', d.AML, i.AML
FROM inserted i
INNER JOIN taskconstructs d on d.ResourceID = i.ResourceID
--update actual data
UPDATE t
SET t.AML = i.AML
from taskconstructs t
INNER JOIN INSERTED i ON i.ResourceID = t.ResourceID
END
GO
SQLFiddle DEMO

Related

How to create a trigger to decrease a counter, what's wrong with my trigger?

I need to decrease a counter in a table schedules, when there was an insert in enrollments table:
CREATE TRIGGER [UpdateEnrollmentsTrigger]
ON [TBLENROLLMENT_ENR]
FOR INSERT
AS
BEGIN
DECLARE #ScheduleCode NVARCHAR
DECLARE #TotalSlots INT
IF EXISTS(SELECT SCH_CODE FROM inserted)
BEGIN
SELECT #ScheduleCode = SCH_CODE FROM inserted
SELECT #TotalSlots = SCH_TOTALSLOTS FROM TBLSCHEDULES_SCH
WHERE SCH_CODE = #ScheduleCode
UPDATE TBLSCHEDULES_SCH
SET SCH_FREESLOTS = #TotalSlots - 1
WHERE SCH_CODE = #ScheduleCode
END
END
When I trying to create this trigger, the query window of VS12 says:
SQL46010 :: Incorrect syntax near ].
Thanks in advance.
The specific error is because you are using FOR INSERT instead of AFTER INSERT, but there are other things that you should improve on your trigger.
First of all, always, always write the length of a NVARCHAR, leaving it blank will behave differently depending where it's used. So replace DECLARE #ScheduleCode NVARCHAR with DECLARE #ScheduleCode NVARCHAR(n), where n is the required length.
I'm also not sure why you are doing the IF EXISTS since it's reading the INSERTED pseudo table, that it's bound to have records because the trigger was fired.
Another thing to improve is that you are assuming that only one row was inserted, as you are storing it on a variable, that's wrong and it will behave incorrectly if you insert more than just one row.
Oh, I almost forgot, you should also always specify the schema, for instance: CREATE TRIGGER [UpdateEnrollmentsTrigger] ON [TBLENROLLMENT_ENR] should be CREATE TRIGGER dbo.[UpdateEnrollmentsTrigger] ON dbo.[TBLENROLLMENT_ENR] (using the correct schema, of course)

Trigger on update of an attribute of a table in SQL Server?

I created computer audit application. When I ran my application, it shows computer accessories on browser like computerName, osVersion, lastAudit, model, totalMemory, processor, userName.
I have created a database in SQL Server 2008 with one table Computers. When a value is inserted into that table, I need to update the table value in the column. In an attempt to try this, I'm using a trigger. However, I do not fully understand how triggers work.
Can someone please show me how to accomplish this.
My table has these columns:
id, computerName, osVersion, lastAudit, model, totalMemory, processor, userName
I know that in this code something wrong or missing but I am not able to complete this. Please help me in this regard.
CREATE TRIGGER update_trigger
ON computers
AFTER UPDATE
AS
BEGIN
declare #id as int
declare #computerName as varchar(100)
declare #osVersion as varchar(100)
declare #lastAudit as datetime
declare #model as varchar(100)
declare #totalMemory float
declare #processor as varchar(100)
declare #userName as varchar(100)
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
if update(id)
BEGIN
insert into computers values(#id,#computerName,#osVersion,#lastAudit,#model,
#totalMemory,#processor,#userName,'Update')
SET NOCOUNT ON;
END
GO
If you want to simply update one or more columns of your existing table when new rows are being inserted (or when they are updated? Not quite clear...), try a trigger like this:
CREATE TRIGGER trigUpdateTable
ON dbo.Computers
AFTER INSERT -- or AFTER UPATE or AFTER INSERT, UPDATE
AS
BEGIN
-- do whatever you want to do on INSERT and/or UPDATE
UPDATE
dbo.Computers
SET
LastAudit = GETDATE()
FROM
dbo.Computers c
INNER JOIN
Inserted i ON c.id = i.id
One very important point to remember: SQL Server triggers are not called per row that is affected - but per statement, and if your INSERT or UPDATE statement affects multiple rows, you'll have multiple entries in the Inserted pseudo table and you need to be able to deal with that fact in your trigger

Forbid insert into table on certain conditions

I have a SQL Server 2008 database. There are three terminals connected to it (A, B, C). There is a table SampleTable in the database, which reacts to any terminal activity. Every time there is some activity on any terminal, logged on to this DB, the new row is inserted into SampleTable.
I want to redirect traffic from one (C) of the three terminals to write to table RealTable and not SampleTable, but I have to do this on DB layer since services that write terminal activity to DB are in Black Box.
I already have some triggers working on SampleTable with the redirecting logic, but the problem is that rows are still being inserted into SampleTable.
What is the cleanest solution for this. I am certain that deleting rows in an inserting trigger is bad, bad, bad.
Please help.
Edit:
Our current logic is something like this (this is pseudo code):
ALTER TRIGGER DiffByTerminal
ON SampleTable
AFTER INSERT
AS
DECLARE #ActionCode VARCHAR(3),
#ActionTime DATETIME,
#TerminalId INT
SELECT #ActionCode = ins.ActionCode,
#ActionTime = ins.ActionTime,
#TerminalId = ins.TerminalId
FROM inserted ins
IF(#TerminalId = 'C')
BEGIN
INSERT INTO RealTable
(
...
)
VALUES
(
#ActionCode,
#ActionTime,
#TerminalId
)
END
In order to "intercept" something before a row gets inserted into a table, you need an INSTEAD OF trigger, not an AFTER trigger. So you can drop your existing trigger (which also included flawed logic that assumed all inserts would be single-row) and create this INSTEAD OF trigger instead:
DROP TRIGGER DiffByTerminal;
GO
CREATE TRIGGER dbo.DiffByTerminal
ON dbo.SampleTable
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.RealTable(...) SELECT ActionCode, ActionTime, TerminalID
FROM inserted
WHERE TerminalID = 'C';
INSERT dbo.SampleTable(...) SELECT ActionCode, ActionTime, TerminalID
FROM inserted
WHERE TerminalID <> 'C';
END
GO
This will handle single-row inserts and multi-row inserts consisting of (a) only C (b) only non-C and (c) a mix.
One of the easiest solution for you is INSTEAD OF trigger. Simply stating, it's trigger that "fires" on very action you decide and lets you "override" the default behavior of the action.
You can override the INSERT, DELETE and UPDATE statements for specific table/view (you use it a lot with views that combine data from different tables and you want make the view insert-able) using INSTEAD OF trigger, where you can put your logic. inside the trigger you can then call again to INSERT when it's appropriate, and you don't have to worry about recursion - INSTEAD OF triggers won't apply on statements from inside the trigger code itself.
Enjoy.

T-SQL: How to deny update on one column of a table via trigger?

Question:
In our SQL-Server 2005 database, we have a table T_Groups.
T_Groups has, amongst other things, the fields ID (PK) and Name.
Now some idiot in our company used the name as key in a mapping table...
Which means now one may not alter a group name, because if one does, the mapping is gone...
Now, until this is resolved, I need to add a restriction to T_Groups, so one can't update the group's name.
Note that insert should still be possible, and an update that doesn't change the groupname should also be possible.
Also note that the user of the application & the developers have both dbo and sysadmin rights, so REVOKE/DENY won't work.
How can I do this with a trigger ?
CREATE TRIGGER dbo.yournametrigger ON T_Groups
FOR UPDATE
AS
BEGIN
IF UPDATE(name)
BEGIN
ROLLBACK
RAISERROR('Changes column name not allowed', 16, 1);
END
ELSE
BEGIN
--possible update that doesn't change the groupname
END
END
CREATE TRIGGER tg_name_me
ON tbl_name
INSTEAD OF UPDATE
AS
IF EXISTS (
SELECT *
FROM INSERTED I
JOIN DELETED D ON D.PK = I.PK AND ISNULL(D.name,I.name+'.') <> ISNULL(I.name,D.name+'.')
)
RAISERROR('Changes to the name in table tbl_name are NOT allowed', 16,1);
GO
Depending on your application framework for accessing the database, a cheaper way to check for changes is Alexander's answer. Some frameworks will generate SQL update statements that include all columns even if they have not changed, such as
UPDATE TBL
SET name = 'abc', -- unchanged
col2 = null, -- changed
... etc all columns
The UPDATE() function merely checks whether the column is present in the statement, not whether its value has changed. This particular statement will raise an error using UPDATE() but won't if tested using the more elaborate trigger as shown above.
This is an example of preserving some original values with an update trigger.
It works by setting the values for orig_author and orig_date to the values from the deleted pseudotable each time. It still performs the work and uses cycles.
CREATE TRIGGER [dbo].[tru_my_table] ON [dbo].[be_my_table]
AFTER UPDATE
AS
UPDATE [dbo].[be_my_table]
SET
orig_author = deleted.orig_author
orig_date = deleted.orig_date,
last_mod_by = SUSER_SNAME(),
last_mod_dt = getdate()
from deleted
WHERE deleted.my_table_id IN (SELECT DISTINCT my_table_id FROM Inserted)
ALTER TABLE [dbo].[be_my_table] ENABLE TRIGGER [tru_my_table]
GO
This example will lock any updates on SABENTIS_LOCATION.fk_sabentis_location through a trigger, and will output a detailed message indicating what objects are affected
ALTER TRIGGER dbo.SABENTIS_LOCATION_update_fk_sabentis_location ON SABENTIS_LOCATION
FOR UPDATE
AS
DECLARE #affected nvarchar(max)
SELECT #affected=STRING_AGG(convert(nvarchar(50), a.id), ', ')
FROM inserted a
JOIN deleted b ON a.id = b.id
WHERE a.fk_sabentis_location != b.fk_sabentis_location
IF #affected != ''
BEGIN
ROLLBACK TRAN
DECLARE #message nvarchar(max) = CONCAT('Update values on column fk_sabentis_location locked by custom trigger. Could not update entities: ', #affected);
RAISERROR(#message, 16, 1)
END
Some examples seem to be using:
IF UPDATE(name)
But this seems to evaluate to TRUE if the field is part of the update statement, even if the value itself has NOT CHANGED leading to false positives.

Help with SQL Server Trigger to truncate bad data before insert

We consume a web service that decided to alter the max length of a field from 255. We have a legacy vendor table on our end that is still capped at 255. We are hoping to use a trigger to address this issue temporarily until we can implement a more business-friendly solution in our next iteration.
Here's what I started with:
CREATE TRIGGER [mySchema].[TruncDescription]
ON [mySchema].[myTable]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [mySchema].[myTable]
SELECT SubType, type, substring(description, 1, 255)
FROM inserted
END
However, when I try to insert on myTable, I get the error:
String or binary data would be
truncated. The statement has been
terminated.
I tried experimenting with SET ANSI_WARNINGS OFF which allowed the query to work but then simply didn't insert any data into the description column.
Is there any way to use a trigger to truncate the too-long data or is there another alternative that I can use until a more eloquent solution can be designed? We are fairly limited in table modifications (i.e. we can't) because it's a vendor table, and we don't control the web service we're consuming so we can't ask them to fix it either. Any help would be appreciated.
The error cannot be avoided because the error is happening when the inserted table is populated.
From the documentation:
http://msdn.microsoft.com/en-us/library/ms191300.aspx
"The format of the inserted and deleted tables is the same as the format of the table on which the INSTEAD OF trigger is defined. Each column in the inserted and deleted tables maps directly to a column in the base table."
The only really "clever" idea I can think of is to take advantage of schemas and the default schema used by a login. If you can get the login that the web service is using to reference another table, you can increase the column size on that table and use the INSTEAD OF INSERT trigger to perform the INSERT into the vendor table. A variation of this is to create the table in a different database and set the default database for the web service login.
CREATE TRIGGER [myDB].[mySchema].[TruncDescription]
ON [myDB].[mySchema].[myTable]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [VendorDB].[VendorSchema].[VendorTable]
SELECT SubType, type, substring(description, 1, 255)
FROM inserted
END
With this setup everything works OK for me.
Not to state the obvious but are you sure there is data in the description field when you are testing? It is possible they change one of the other fields you are inserting as well and maybe one of those is throwing the error?
CREATE TABLE [dbo].[DataPlay](
[Data] [nvarchar](255) NULL
) ON [PRIMARY]
GO
and a trigger like this
Create TRIGGER updT ON DataPlay
Instead of Insert
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [tempdb].[dbo].[DataPlay]
([Data])
(Select substring(Data, 1, 255) from inserted)
END
GO
then inserting with
Declare #d as nvarchar(max)
Select #d = REPLICATE('a', 500)
SET ANSI_WARNINGS OFF
INSERT INTO [tempdb].[dbo].[DataPlay]
([Data])
VALUES
(#d)
GO
I am unable to reproduce this issue on SQL 2008 R2 using:
Declare #table table ( fielda varchar(10) )
Insert Into #table ( fielda )
Values ( Substring('12345678901234567890', 1, 10) )
Please make sure that your field is really defined as varchar(255).
I also strongly suggest you use an Insert statement with an explicit field list. While your Insert is syntactically correct, you really should be using an explicit field list (like in my sample). The problem is when you don't specify a field list you are at the mercy of SQL and the table definition for the field order. When you do use a field list you can change the order of the fields in the table (or add new fields in the middle) and not care about your insert statements.