In trigger, get values from insert - sql-server

When I try to create this trigger, I get this exception:
The name "SITE_ID" is not permitted in this context. Valid expressions are constants, constant expressions, and (in some contexts) variables. Column names are not permitted.
CREATE TRIGGER tr_VISITACTIVITY_insert ON VISITACTIVITY
INSTEAD OF INSERT AS
BEGIN
INSERT INTO [VISITACTIVITY]
([O_ID]
,[SITE_ID]
,[VISIT_DATE]
,[STAFF]
,[VISIT_TYPE_ID]
,[COMMENTS]
,[DELETED])
VALUES
(-1
,SITE_ID
,VISIT_DATE
,STAFF
,VISIT_TYPE_ID
,VISIT_COMMENTS
,0
)
GO
END
GO
The insert that the programmer would issue in the application is something like this:
INSERT INTO [VISITACTIVITY]
([SITE_ID]
,[VISIT_DATE]
,[STAFF_COMMENT]
,[VISIT_TYPE_ID]
,[COMMENTS]
)
VALUES
(15
,'2011-08-08'
,'Eric, John'
,6
,'invasives growing along the border of the preserve'
)
So, I need the values that the programmer provided in the insert to create the insert in the trigger.
How to I reference these values, then, in the trigger?
Thanks.

You need to look at the meta-table INSERTED, which is basically a table that contains all the data being inserted, so you'd reference (for example) SITE_ID as inserted.SITE_ID.
What you also need to be careful of as well is doing single-row inserts from within the trigger (as you seem to be doing). If multiple rows are inserted in a batch, then INSERTED will contain multiple rows. So what you need to do is to use INSERT INTO table (...) SELECT ..., for example:
INSERT INTO [VISITACTIVITY]
([SITE_ID]
,[VISIT_DATE]
,[STAFF_COMMENT]
,[VISIT_TYPE_ID]
,[COMMENTS]
)
SELECT VISITACTIVITY, SITE_ID, VISIT_DATE, STAFF_COMMENT, VISIT_TYPE_ID, COMMENTS
FROM inserted
By the way, reading your question, it looks like you're just using the trigger to populate default values in a couple of columns. Have you considered just using a DEFAULT constraint on the table instead? For example, if you did the following:
ALTER TABLE VisitActivity ADD CONSTRAINT DF_VisitActivity_Deleted DEFAULT (0) FOR DELETED
ALTER TABLE VisitActivity ADD CONSTRAINT DF_VisitActivity_OID DEFAULT (-1) FOR O_ID
...then this would make your example insert behave in the same way without the trigger. It depends whether you always want to override the values in Deleted or O_ID or not. If you always want to override, use a trigger - or better yet, stick a CHECK on the column to force it to a fixed value (will also highlight where your code is trying to change it as SQL Server will fail the insert due to referential integrity). If you want to be able to override it, then use DEFAULT constraints.
(Of course, you may well have simplified the problem for the sake of the question, but I'm just highlighting how you can do this without resorting to a trigger).

Related

Update OUTPUT results when used on a view with an INSTEAD OF INSERT trigger

I have a view that combines two tables. When I do an insert into the view, the trigger gets the primary key/clustered index value using NEXT VALUE FOR MySequence.
The trigger then inserts into the two tables using the generated key and the values in the Inserted table. It all works just fine.
The problem comes in when an application that uses the view does an insert with an output clause. Something like this:
DECLARE #InsertedIds table (MyViewId int, ValueForTheFirstTable int)
INSERT INTO MyView (ValueForTheFirstTable, ValueForTheSecondTable )
OUTPUT Inserted.MyViewId, Inserted.ValueForTheFirstTable into #InsertedIds
VALUES ('FirstTableValue', 'SecondTableValue')
ValueForTheFirstTable does return the correct value. But the MyViewId (the key) is null. (And I really expect it to be anything else because it does not know the key that I generated.)
My question is, how can I modify my INSTEAD OF INSERT trigger to make it so that the OUTPUT clause will contain my generated Sequence number value?
I know value, I just need a way to feed it back to SQL Server so it can provide it to any OUTPUT clauses.

How to emulate a BEFORE INSERT trigger in T-SQL / SQL Server for super/subtype (Inheritance) entities? [duplicate]

This question already has answers here:
How can I do a BEFORE UPDATED trigger with sql server?
(9 answers)
Closed 2 years ago.
This is on Azure.
I have a supertype entity and several subtype entities, the latter of which needs to obtain their foreign keys from the primary key of the super type entity on each insert. In Oracle, I use a BEFORE INSERT trigger to accomplish this. How would one accomplish this in SQL Server / T-SQL?
DDL
CREATE TABLE super (
super_id int IDENTITY(1,1)
,subtype_discriminator char(4) CHECK (subtype_discriminator IN ('SUB1', 'SUB2')
,CONSTRAINT super_id_pk PRIMARY KEY (super_id)
);
CREATE TABLE sub1 (
sub_id int IDENTITY(1,1)
,super_id int NOT NULL
,CONSTRAINT sub_id_pk PRIMARY KEY (sub_id)
,CONSTRAINT sub_super_id_fk FOREIGN KEY (super_id) REFERENCES super (super_id)
);
I wish for an insert into sub1 to fire a trigger that actually inserts a value into super and uses the super_id generated to put into sub1.
In Oracle, this would be accomplished by the following:
CREATE TRIGGER sub_trg
BEFORE INSERT ON sub1
FOR EACH ROW
DECLARE
v_super_id int; //Ignore the fact that I could have used super_id_seq.CURRVAL
BEGIN
INSERT INTO super (super_id, subtype_discriminator)
VALUES (super_id_seq.NEXTVAL, 'SUB1')
RETURNING super_id INTO v_super_id;
:NEW.super_id := v_super_id;
END;
Please advise on how I would simulate this in T-SQL, given that T-SQL lacks the BEFORE INSERT capability?
Sometimes a BEFORE trigger can be replaced with an AFTER one, but this doesn't appear to be the case in your situation, for you clearly need to provide a value before the insert takes place. So, for that purpose, the closest functionality would seem to be the INSTEAD OF trigger one, as #marc_s has suggested in his comment.
Note, however, that, as the names of these two trigger types suggest, there's a fundamental difference between a BEFORE trigger and an INSTEAD OF one. While in both cases the trigger is executed at the time when the action determined by the statement that's invoked the trigger hasn't taken place, in case of the INSTEAD OF trigger the action is never supposed to take place at all. The real action that you need to be done must be done by the trigger itself. This is very unlike the BEFORE trigger functionality, where the statement is always due to execute, unless, of course, you explicitly roll it back.
But there's one other issue to address actually. As your Oracle script reveals, the trigger you need to convert uses another feature unsupported by SQL Server, which is that of FOR EACH ROW. There are no per-row triggers in SQL Server either, only per-statement ones. That means that you need to always keep in mind that the inserted data are a row set, not just a single row. That adds more complexity, although that'll probably conclude the list of things you need to account for.
So, it's really two things to solve then:
replace the BEFORE functionality;
replace the FOR EACH ROW functionality.
My attempt at solving these is below:
CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
DECLARE #new_super TABLE (
super_id int
);
INSERT INTO super (subtype_discriminator)
OUTPUT INSERTED.super_id INTO #new_super (super_id)
SELECT 'SUB1' FROM INSERTED;
INSERT INTO sub (super_id)
SELECT super_id FROM #new_super;
END;
This is how the above works:
The same number of rows as being inserted into sub1 is first added to super. The generated super_id values are stored in a temporary storage (a table variable called #new_super).
The newly inserted super_ids are now inserted into sub1.
Nothing too difficult really, but the above will only work if you have no other columns in sub1 than those you've specified in your question. If there are other columns, the above trigger will need to be a bit more complex.
The problem is to assign the new super_ids to every inserted row individually. One way to implement the mapping could be like below:
CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
DECLARE #new_super TABLE (
rownum int IDENTITY (1, 1),
super_id int
);
INSERT INTO super (subtype_discriminator)
OUTPUT INSERTED.super_id INTO #new_super (super_id)
SELECT 'SUB1' FROM INSERTED;
WITH enumerated AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rownum
FROM inserted
)
INSERT INTO sub1 (super_id, other columns)
SELECT n.super_id, i.other columns
FROM enumerated AS i
INNER JOIN #new_super AS n
ON i.rownum = n.rownum;
END;
As you can see, an IDENTIY(1,1) column is added to #new_user, so the temporarily inserted super_id values will additionally be enumerated starting from 1. To provide the mapping between the new super_ids and the new data rows, the ROW_NUMBER function is used to enumerate the INSERTED rows as well. As a result, every row in the INSERTED set can now be linked to a single super_id and thus complemented to a full data row to be inserted into sub1.
Note that the order in which the new super_ids are inserted may not match the order in which they are assigned. I considered that a no-issue. All the new super rows generated are identical save for the IDs. So, all you need here is just to take one new super_id per new sub1 row.
If, however, the logic of inserting into super is more complex and for some reason you need to remember precisely which new super_id has been generated for which new sub row, you'll probably want to consider the mapping method discussed in this Stack Overflow question:
Using merge..output to get mapping between source.id and target.id
While Andriy's proposal will work well for INSERTs of a small number of records, full table scans will be done on the final join as both 'enumerated' and '#new_super' are not indexed, resulting in poor performance for large inserts.
This can be resolved by specifying a primary key on the #new_super table, as follows:
DECLARE #new_super TABLE (
row_num INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
super_id int
);
This will result in the SQL optimizer scanning through the 'enumerated' table but doing an indexed join on #new_super to get the new key.

SQL server trigger question

I am by no means a sql programmer and I am trying to accomplish something that I am pretty sure has been done a million times before.
I am trying to auto generate a customer number in sql every time a new customer is inserted, but the trigger (or sp?) will only work if at least the first name, last name and another value called case number is entered. If any of these fields are missing, the system generates an error. If the criteria is met, the system generates and assigns a unique id to that customer that begins with letters GL- and then uses 5 digit number so a customer John Doe would be GL-00001 and Jane Doe would be GL-00002.
I am sorry if I am asking too much but I am basically a select insert update guy and nothing more so thanks in advance for any help.
If I were in this situation, I would:
--Alter the table(s) so that first name, last name and case number are required (NOT NULL) columns. Handle your checks for required fields on the application side before submitting the record to the database.
--If it doesn't already exist, add an identity column to the customer table.
--Add a persisted computed column to the customer table that will format the identity column into the desired GL-00000 format.
/* Demo computed column for customer number */
create table #test (
id int identity,
customer_number as 'GL-' + left('00000', 5-len(cast(id as varchar(5)))) + cast(id as varchar(5)) persisted,
name char(20)
)
insert into #test (name) values ('Joe')
insert into #test (name) values ('BobbyS')
select * from #test
drop table #test
This should satisfy your requirements without the need to introduce the overhead of a trigger.
So what do you want to do? generate a customer number even when these fields arn't populated?
Have you looked at the SQL for the trigger? You can do this in SSMS (SQL Server Managment Studio) by going to the table in question in the Object Explorer, expanding the table and then expanding triggers.
If you open up the trigger you'll see what it does to generate the customer number. If you are unsure on how this code works, then post the code for the trigger up.
If you are making changes to an existing system i'd advise you to find out any implications that changing the way data is inputted works.
For example, others parts of the application may depend on all of the initial values being populated, so after changing the trigger to allow incomplete data to be added, you may inturn break something else.
You have probably a unique constraint and/or NOT NULL constraints set on the table.
Remove/Disable these (for example with the SQL-Server Management Console in Design Mode) and then try again to insert the data. Keep in mind, that you will probably not be able to enable the constraints after your insert, since you are violating conditions after the insert. Only disable or reomve the constraints, if you are absolutely sure that they are unecessary.
Here's example syntax (you need to know the constraint names):
--disable
ALTER TABLE customer NOCHECK CONSTRAINT your_constraint_name
--enable
ALTER TABLE customer CHECK CONSTRAINT your_constraint_name
Caution: If I were you, I'd rather try to insert dummy values for the not null columns like this:
insert into customers select afield , 1 as dummyvalue, 2 as dummyvalue from your datasource
A very easy way to do this would be to create a table of this sort of structure:
CustomerID of type in that is a primary key and set it as identity
CustomerIDPrfix of type varchar(3) which stores GL- as a default value.
Then add your other fields and set them to NOT NULL.
If that way is not acceptable and you do need to write a trigger check out these two articles:
http://msdn.microsoft.com/en-us/library/aa258254(SQL.80).aspx
http://www.kodyaz.com/articles/sql-trigger-example-in-sql-server-2008.aspx
Basiclly it is all about getting the logic right to check if the fields are blank. Experiment with a test database on your local machine. This will help you get it right.

INSERT TRIGGERS and the UPDATE() function

I have created an INSTEAD OF INSERT trigger on a view in my database. I want to know which columns are included in the column list of the INSERT statement on the view.
If you read the MSDN documentation for triggers the UPDATE() and COLUMNS_UPDATED() functions should satisfy this requirement. However, during my testing I found that regardless of what columns are in the INSERT column list the UPDATE() and COLUMNS_UPDATED() functions always return all columns from the view.
CREATE VIEW dbo.MyView (BatchId, [Status], OrderNumber, WhenClosed) AS
SELECT bth.BatchId, bth.[Status], bth.OrderNumber,
Private.ufxAdjustDateTime(bth.WhenClosed, bth.WhenClosedUtcOffset)
FROM Private.Batch AS bth
GO
CREATE TRIGGER dbo.[MyView-Insert] ON dbo.MyView INSTEAD OF INSERT AS
BEGIN
SET NOCOUNT ON
DECLARE #batchIdIsSet BIT
SELECT #batchIdIsSet = 0
IF UPDATE(BatchId)
SELECT #batchIdIsSet = 1
INSERT INTO Private.Batch
(BatchId, [Status], OrderNumber)
SELECT CASE #batchIdSet
WHEN 1 THEN ins.BatchId
ELSE NEWID()
END, ins.[Status], ins.OrderNumber
FROM inserted AS ins
END
The reason I want to do this is that I need to modify an existing table and I have loads of legacy code that relies on it. So what I've done is created a new table, changed the old table to a view and created triggers, on the view, to allow INSERT, UPDATE and DELETE statements.
Now, the old table had defaults for certain columns, if the insert into the view uses the default I want to use a default in my insert into the new table. To do this I have to be able to figure out which columns had values [explicitly] supplied for the INSERT.
Checking to see if the column has NULL is not enough because the INSERT statement can explicitly set the field value to NULL and this is perfectly acceptable.
Hmmm, I hope this is clear.
Kep.
On an INSERT statement, every column is affected. It either gets NULL, or the value you're specifying.
Checking for NULL would be the best option, but as you can't do that, I'm thinking you might be a bit stuck. Can you work out scenarios which might need to handle NULL explicitly?
For INSERT, everything is a change because it's a new row.
If you had an AFTER trigger, you could test to see if the inserted value is the default value. If the default is NULL (eg nullable and no default), then how can you distinguish if NULL is explicitly inserted in any trigger?
In a BEFORE trigger, I don't know if you can trap the default. Of course, if the default is NEWID() this still won't help you.
On the face of it, this can't be done in a trigger.

Detailed error message for violation of Primary Key constraint in sql2008?

I'm inserting a large amount of rows into an empty table with a primary key constraint on one column.
If there is a duplicate key error, is there any way to find out the value of the key (or row) that caused the error?
Validating the data prior to the insert is sadly not something I can do right now.
Using SQL 2008.
Thanks!
Doing the count(*) / group by thing is something I'm trying to avoid, this is an insert of hundreds of millions of rows from hundreds of different DB's (some of which are on remote servers)...I don't have the time or space to do the insert twice.
The data is supposed to be unique from the providers, but unfortunately their validation doesn't seem to work correctly 100% of the time and I'm trying to at least see where it's failing so I can help them troubleshoot.
Thank you!
There's not a way of doing it that won't slow your process down, but here's one way that will make it easier. You can add an instead-of trigger on that table for inserts and updates. The trigger will check each record before inserting it and make sure it won't cause a primary key violation. You can even create a second table to catch violations, and have a different primary key (like an identity field) on that one, and the trigger will insert the rows into your error-catching table.
Here's an example of how the trigger can work:
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
In that example, I'm checking a field to make sure it's numeric before I insert the data into the table. You'll need to modify that code to check for primary key violations instead - for example, you might join the INSERTED table to your own existing table and only insert rows where you don't find a match.
The solution would depend on how often this happens. If it's <10% of the time then I would do the following:
Insert the data
If error then do Bravax's revised solution (remove constraint, insert, find dup, report and kill dup, enable constraint).
This means it's only costing you on the few times an error occurs.
If this is happening more often then I'd look at sending the boys over to see the providers :-)
Revised:
Since you don't want to insert twice, could you:
Drop the primary key constraint.
Insert all data into the table
Find any duplicates, and remove them
Then re-add the primary key constraint
Previous reply:
Insert the data into a duplicate of the table without the primary key constraint.
Then run a query on it to determine rows which have duplicate values for the rpimary key column.
select count(*), <Primary Key>
from table
group by <Primary Key>
having count(*) > 1
Use SSIS to import the data and have it check for this as part of the data flow. That is the best way to handle. SSIS can send the bad records to a table (that you can later send to the vendor to help them clean up their act) and process the good ones.
I can't believe that SSIS does not easily address this "reality", because, let's face it, oftentimes you need and want to be able to:
See if a record exists with a certain unique or primary key
If it does not, insert it
If it does, either ignore it or update it.
I don't understand how they would let a product out the door without this capability built-in in an easy-to-use manner. Like, say, set an attribute of a component to automatically check this.

Resources