SQL Server : trigger on Insert alter input - sql

I am fairly new to SQL Server and I am struggling with creating a trigger that does what I need.
We have a table that before a record is inserted we want to manipulate the input value.
Example table:
MyTable
(
[RUID] INT IDENTITY(1,1),
[CustomerName] nvarchar(200),
[CustomerStatus] INT,
[CustomerType] char(1),
[OtherFields] nvarchar(100)
)
An insert may come in like:
INSERT INTO MyTable ([CustomerName], [CustomerStatus], [CustomerType], [OtherFields])
VALUES ('LName~FName', 2, 'A', 'Other Info')
We don't have control of the source system doing the insert (a vendor product that is on its way out in a couple years so management doesn't want to spend the sums of money to have them alter it) but we need something like this to happen.
CustomerStatus that is inserted to be 2x inserted value
CustomerType - Regardless of the value sent we want to be overridden with a value of 'B'
All other columns are left as is.
So with an insert sent with the values in the example above, we would actually want this to end up in the table:
'LName~FName', 4, 'B', 'Other Info'
Any help you could provide would be greatly appreciated.
Specs:
SQL Server 2008 R2 Standard Edition (Database is in SQL Server 2000 compatibility mode though)

You basically need an Instead of trigger. The code would look something like...
CREATE TRIGGER tr_Insert_MyTable
ON MyTable
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO MyTable ([CustomerName], [CustomerStatus], [CustomerType], [OtherFields])
SELECT [CustomerName]
, [CustomerStatus] * 2 AS [CustomerStatus]
,'B' [CustomerType]
, [OtherFields]
FROM inserted
END

Related

Cloning a table definition to a table variable in SQL Server

Is there a way to clone the table definition from an existing table and recreate as a table variable?
DECLARE #TempTable1 TABLE (ID INT, Description VARCHAR(256))
I need to recreate a set of tables with same number of columns and definitions without repeating the DECLARE TABLE statement.
This process is available on MySQL as below.
CREATE TABLE TempTable1 LIKE TempTableMain;
Is it possible to do this is Microsoft SQL Server?
Please note that the actual scenario contains more that 60 columns in the #TempTable and need to create more than 10 instances from the original table.
I am not talking about data insertion or SELECT ion from another table as below. I need to create the table definition.
DECLARE #TempTable TABLE(ID INT, Description VARCHAR(100))
INSERT INTO #TempTable
VALUES (1, 'Test1'), (1, 'Test1');
SELECT *
INTO #TempTable2
FROM #TempTable1
SELECT * FROM #TempTable2
Create a user defined type with the columns of your table, lets say like that:
CREATE TYPE MyTableType AS TABLE (ID INT, Description VARCHAR(256));
And then declare your table variables using this type:
DECLARE #Table1 MyTableType;
DECLARE #Table2 MyTableType;
DECLARE #Table3 MyTableType;
SQL Server management studio gives you the option to create a sql script to create an already existing table.
Right click your table -> script table as -> CREATE To -> New Query Editor window
This way you dont have to write out the whole query every single time.
You could even create a stored procedure which takes as argument the name of your to be created table and run this from a while loop.
You can perform the following command:
SELECT * INTO #MyTable_tmp FROM MyTable
Then modify your MyTable, and copy your data back in. Other approaches I've seen is to create a new table calling it Mytable_Tmp (Not a temp table), which will be your new table.
Then copy your data doing any migrations you need. Then you will drop the original table and do a rename on Mytable.
When you run SELECT * INTO #MyTable FROM MyTable, SQL Server creates a new temporary table called #MyTable that matches each column and data type from your select clause. In this case we are selecting * so it will match MyTable. This only creates the columns it doesn't copy defaults, constraints indexes or anything else.
If you are using table variables, it means that you don't want to use them in long period of time, as they will be "forgotten" after every script completion.
So, easiest in my opinion is to use such construct:
IF OBJECT_ID('tempdb.dbo.#tmpTable', 'U') IS NOT NULL
DROP TABLE #tmpTable;
SELECT * INTO #tmpTable FROM MyPrimaryTable
It creates temporary table exactly like yours, if you want empty table, you can just use:
SELECT * INTO #tmpTable FROM MyPrimaryTable WHERE 1 = 0
Then, temporary table will have exact same schema as your primary table.
You can apply as many times as you need (create as many temporary tables as you need).
You could use regular tables instead of temporary tables as well.
If you want to re-create table after dropping the existing table then you can use the below query.
/*
Create brands table
*/
-- Old block of code
IF EXISTS (SELECT * FROM sys.objects
WHERE object_id = OBJECT_ID(N'[TOY].[BRANDS]') AND type in (N'U'))
DROP TABLE [TOY].[BRANDS]
GO
-- New block of code
DROP TABLE IF EXISTS [TOY].[BRANDS]
GO
-- Add new table
CREATE TABLE TOY.BRANDS
(
ID INT NOT NULL,
NAME VARCHAR(20) NULL
)
GO
-- Load the table with data
INSERT INTO TOY.BRANDS (ID, NAME) VALUES
(1, 'Ford'),
(2, 'Chevy'),
(3, 'Dodge'),
(4, 'Plymouth'),
(5, 'Oldsmobile'),
(6, 'Lincoln'),
(7, 'Mercury');
GO

SQL Server : setting the value to the next highest id number when the table is not set to auto increment

I am using SQL Server 2008 and some times SQL Server 2012 and I need to do an insert on a pre-existing database where the schema does not seem to have the id column set for auto-increment.
So for example, here is a basic table My_Table that I have:
[Id] [Name]
-------------
1 JOHN
2 BOB
3 SALLY
I need to do an insert to get the MAX value of the last Id and then use this value for my new insert statement. I believe this is the best approach.
So for example the "pseudo syntax" for the insert would be:
INSERT INTO My_Table
VALUES(MAX(Id) + 1, 'MIKE');
How is the best way to do this when the Id is not set in the table schema to auto increment?
Note: I am using Microsoft SQL Server.
Thanks for any advice.
I recommend changing the structure of the table to use an identity column. The table definition should be:
create table my_table (
id int identity(1, 1) primary key,
. . .
);
This is the right solution. If you can't do that, you can express the logic as:
insert to my_table (id, name)
select coalesce(1 + max(id), 0), 'Mike'
from my_table;
However, this suffers from race conditions. Two threads could attempt an insert at the same time and end up with the same id. Avoiding such race conditions is why you want the database to do the work.
If you are in control of all inserts into the table, you can use a sequence as well.
You could create another table with an IDENTITY column:
CREATE TABLE ID_Insert (
ID INT IDENTITY(234, 1) primary key,
Val smallint null
)
(Where your seed value will be MAX(ID) + 1)
Insert any value into this table:
Insert ID_Insert(Val) values(NULL)
Use SCOPE_IDENTITY() to get the ID of the inserted value and use that in your insert into your other table.
NOTE: I have not tested this, but it gets around all the issues raised so far, so any criticism is welcome.
If you can't change the table structure so that the Id is an IDENTITY column, then this is probably your best bet:
SET XACT_ABORT ON;
BEGIN TRANSACTION;
DECLARE #maxId int = (SELECT MAX(id) FROM my_table);
INSERT INTO my_table (id, name)
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) + #maxId, name
FROM my_other_table;
COMMIT TRANSACTION;
This works for batch inserts, not only single name inserts.

Conditional INSERT subquery of larger insert

I have a set of tables which track access logs. The logs contain data about the user's access including user agent strings. Since we know that user agent strings are, for all intents and purposes, practically unlimited, these would need to be stored as a text/blob type. Given the high degree of duplication, I'd like to store these in a separate reference table and have my main access log table have an id linking to it. Something like this:
accesslogs table:
username|accesstime|ipaddr|useragentid
useragents table:
id|crc32|md5|useragent
(the hashes are for indexing and quicker searching)
Here's the catch, i am working inside a framework that doesn't give me access to create fancy things like foreign keys. In addition, this has to be portable across multiple DBMSs. I have the join logic worked out for doing SELECTS but I am having trouble figuring out how to insert properly. I want to do something like
INSERT INTO accesslogs (username, accesstime, ipaddr, useragentid)
VALUES
(
:username,
:accesstime,
:ipaddr,
(
CASE WHEN
(
SELECT id
FROM useragents
WHERE
useragents.crc32 = :useragentcrc32
AND
useragents.md5 = :useragentmd5
AND useragents.useragent LIKE :useragent
) IS NOT NULL
THEN
THAT_SAME_SELECT_FROM_ABOVE()
ELSE
GET_INSERT_ID_FROM(INSERT INTO useragents (crc32, md5, useragent) VALUES (:useragentcrc32, :useragentmd5, :useragent))
)
)
Is there any way to do this that doesn't use pseudofunctions whose names i just made up? The two parts i'm missing is how to get the select from above and how to get the new id from a subquery insert.
You will need to do separate inserts to each of the tables. You can not do insert into both at the same time.
If you use MS SQL Server once you inserted you can get inserted id by SCOPE_IDENTITY(), and then use it in another table insert.
I'm not sure there is a cross platform way of doing this. You may have to have a lot of special cases for each supported back end. For Example, for SQL Server you'd use the merge statement as the basis of the solution. Other DBMSs have different names if they support it at all. Searching for "Upsert" might help.
Edt - added the second query to be explicit, and added parameters.
-- SQL Server Example
--Schema Defs
Create Table Test (
id int not null identity primary key,
UserAgent nvarchar(50)
)
Create Table WebLog (
UserName nvarchar(50),
APAddress nvarchar(50),
UserAgentID int
)
Create Unique Index UQ_UserAgent On Test(UserAgent)
-- Values parsed from log
Declare
#UserName nvarchar(50) = N'Loz',
#IPAddress nvarchar(50) = N'1.1.1.1',
#UserAgent nvarchar(50) = 'Test'
Declare #id int
-- Optionally Begin Transaction
-- Insert if necessary and get id
Merge
Into dbo.Test as t
Using
(Select #UserAgent as UserAgent) as s
On
t.[UserAgent] = s.[UserAgent]
When Matched Then
Update Set #id = t.id
When Not Matched Then
Insert (UserAgent) Values (s.UserAgent);
If #id Is Null Set #id = scope_identity()
Insert Into WebLog (UserName, IPAddress, UserAgentID) Values (#UserName, #IPAddress, #id)
-- Optionally Commit Transaction

Get IDENTITY value in the same T-SQL statement it is created in?

I was asked if you could have an insert statement, which had an ID field that was an "identity" column, and if the value that was assigned could also be inserted into another field in the same record, in the same insert statement.
Is this possible (SQL Server 2008r2)?
Thanks.
You cannot really do this - because the actual value that will be used for the IDENTITY column really only is fixed and set when the INSERT has completed.
You could however use e.g. a trigger
CREATE TRIGGER trg_YourTableInsertID ON dbo.YourTable
AFTER INSERT
AS
UPDATE dbo.YourTable
SET dbo.YourTable.OtherID = i.ID
FROM dbo.YourTable t2
INNER JOIN INSERTED i ON i.ID = t2.ID
This would fire right after any rows have been inserted, and would set the OtherID column to the values of the IDENTITY columns for the inserted rows. But it's strictly speaking not within the same statement - it's just after your original statement.
You can do this by having a computed column in your table:
DECLARE #QQ TABLE (ID INT IDENTITY(1,1), Computed AS ID PERSISTED, Letter VARCHAR (1))
INSERT INTO #QQ (Letter)
VALUES ('h'),
('e'),
('l'),
('l'),
('o')
SELECT *
FROM #QQ
1 1 h
2 2 e
3 3 l
4 4 l
5 5 o
About the cheked answer:
You cannot really do this - because the actual value that will be used
for the IDENTITY column really only is fixed and set when the INSERT
has completed.
marc_s I suppose, you are not actually right. Yes, He can! ))
The way to solution is IDENT_CURRENT():
CREATE TABLE TemporaryTable(
Id int PRIMARY KEY IDENTITY(1,1) NOT NULL,
FkId int NOT NULL
)
ALTER TABLE TemporaryTable
ADD CONSTRAINT [Fk_const] FOREIGN KEY (FkId) REFERENCES [TemporaryTable] ([Id])
INSERT INTO TemporaryTable (FkId) VALUES (IDENT_CURRENT('[TemporaryTable]'))
INSERT INTO TemporaryTable (FkId) VALUES (IDENT_CURRENT('[TemporaryTable]'))
INSERT INTO TemporaryTable (FkId) VALUES (IDENT_CURRENT('[TemporaryTable]'))
INSERT INTO TemporaryTable (FkId) VALUES (IDENT_CURRENT('[TemporaryTable]'))
UPDATE TemporaryTable
SET [FkId] = 3
WHERE Id = 2
SELECT * FROM TemporaryTable
DROP TABLE TemporaryTable
More over, you can even use IDENT_CURRENT() as DEFAULT CONSTRAINT and it works instead of SCOPE_IDENTITY() for example. Try this:
CREATE TABLE TemporaryTable(
Id int PRIMARY KEY IDENTITY(1,1) NOT NULL,
FkId int NOT NULL DEFAULT IDENT_CURRENT('[TemporaryTable]')
)
ALTER TABLE TemporaryTable
ADD CONSTRAINT [Fk_const] FOREIGN KEY (FkId) REFERENCES [TemporaryTable] ([Id])
INSERT INTO TemporaryTable (FkId) VALUES (DEFAULT)
INSERT INTO TemporaryTable (FkId) VALUES (DEFAULT)
INSERT INTO TemporaryTable (FkId) VALUES (DEFAULT)
INSERT INTO TemporaryTable (FkId) VALUES (DEFAULT)
UPDATE TemporaryTable
SET [FkId] = 3
WHERE Id = 2
SELECT * FROM TemporaryTable
DROP TABLE TemporaryTable
You can do both.
To insert rows with a column "identity", you need to set identity_insert off.
Note that you still can't duplicate values!
You can see the command here.
Be aware to set identity_insert on afterwards.
To create a table with the same record, you simply need to:
create new column;
insert it with null value or other thing;
update that column after inserts with the value of the identity column.
If you need to insert the value at the same time, you can use the ##identity global variable. It'll give you the last inserted. So I think you need to do a ##identity + 1. In this case it can give wrong values because the ##identity is for all tables. So it'll count if the insert occurs in another table with identity.
Another solution is to get the max id and add one :) and you get the needed value!
use this simple code
`SCOPE_IDENTITY()+1
I know the original post was a long while ago. But, the top-most solution is using a trigger to update the field after the record has been inserted and I think there is a more efficient method.
Using a trigger for this has always bugged me. It always has seemed like there must be a better way. That trigger basically makes every insert perform 2 writes to the database, (1) the insert, and then (2) the update of the 2nd int. The trigger is also doing a join back into the table. This is a bit of overhead to have especially for a large database and large tables. And I suspect that as the table gets larger, the overhead of this approach does also. Maybe I'm wrong on that. But, it just doesn't seem like a good solution on a large table.
I wrote a function fn_GetIdent that can be used for this. It's funny how simple it is but really was some work to figure out. I stumbled onto this eventually. It turns out that calling IDENT_CURRENT(#variableTableName) from within a function that is called from the INSERT statements SET value assignment clause acts differently than if you call IDENT_CURRENT(#variableTableName) from the INSERT statement directly. And it makes it where you can get the new identity value for the record that you are inserting.
There is one caveat. When the identity is NULL (ie - an empty table with no records) it acts a little differently since the sys.identity_columns.last_value is NULL. So, you have to handle the very first record entered a little differently. I put code in the function to address that, and now it works.
This works because each call to the function, even within the same INSERT statement, is in it's own new "scope" within the function. (I believe that is the correct explanation). So, you can even insert multiple rows with one INSERT statement using this function. If you call IDENT_CURRENT(#variableTableName) from the INSERT statement directly, it will assign the same value for the newID in all rows. This is because the identity gets updated after the entire INSERT statement finishes processing (within the same scope). But, calling IDENT_CURRENT(#variableTableName) from within a function causes each insert to update the identity value with each row entered. But, it's all done in a function call from the INSERT statement itself. So, it's easy to implement once you have the function created.
This approach is a call to a function (from the INSERT statement) which does one read from the sys.identity_columns.last_value (to see if it is NULL and if a record exists) within the function and then calling IDENT_CURRENT(#variableTableName) and then returning out of the function to the INSERT statement to insert the row. So, it is one small read (for each row INSERTED) and then the one write of the insert which is less overhead than the trigger approach I think. The trigger approach could be rather inefficient if you use that for all tables in a large database with large tables. I haven't done any performance analysis on it compared to the trigger. But, I think this would be a lot more efficient, especially on large tables.
I've been testing it out and this seems to work in all cases. I would welcome feedback as to whether anyone finds where this doesn't work or if there is any problem with this approach. Can anyone can shoot holes in this approach? If so, please let me know. If not, could you vote it up? I think it is a better approach.
So, maybe being holed up due to COVID-19 out there, turned out to be productive for something. Thank you Microsoft for keeping me occupied. Anyone hiring? :) No, seriously, anyone hiring? OK, so now what am I going to do with myself now that I am done with this? :) Wishing everyone safe times out there.
Here is the code below. Wondering if this approach has any holes in it. Feedback welcomed.
IF OBJECT_ID('dbo.fn_GetIdent') IS NOT NULL
DROP FUNCTION dbo.fn_GetIdent;
GO
CREATE FUNCTION dbo.fn_GetIdent(#inTableName AS VARCHAR(MAX))
RETURNS Int
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #tableHasIdentity AS Int
DECLARE #tableIdentitySeedValue AS Int
/*Check if the tables identity column is null - a special case*/
SELECT
#tableHasIdentity = CASE identity_columns.last_value WHEN NULL THEN 0 ELSE 1 END,
#tableIdentitySeedValue = CONVERT(int, identity_columns.seed_value)
FROM sys.tables
INNER JOIN sys.identity_columns
ON tables.object_id = identity_columns.object_id
WHERE identity_columns.is_identity = 1
AND tables.type = 'U'
AND tables.name = #inTableName;
DECLARE #ReturnValue AS Int;
SET #ReturnValue = CASE #tableHasIdentity WHEN 0 THEN #tableIdentitySeedValue
ELSE IDENT_CURRENT(#inTableName)
END;
RETURN (#ReturnValue);
END
GO
/* The function above only has to be created the one time to be used in the example below */
DECLARE #TableHasRows AS Bit
DROP TABLE IF EXISTS TestTable
CREATE TABLE TestTable (ID INT IDENTITY(1,1),
New INT,
Letter VARCHAR (1))
INSERT INTO TestTable (New, Letter)
VALUES (dbo.fn_GetIdent('TestTable'), 'H')
INSERT INTO TestTable (New, Letter)
VALUES (dbo.fn_GetIdent('TestTable'), 'e')
INSERT INTO TestTable (New, Letter)
VALUES (dbo.fn_GetIdent('TestTable'), 'l'),
(dbo.fn_GetIdent('TestTable'), 'l'),
(dbo.fn_GetIdent('TestTable'), 'o')
INSERT INTO TestTable (New, Letter)
VALUES (dbo.fn_GetIdent('TestTable'), ' '),
(dbo.fn_GetIdent('TestTable'), 'W'),
(dbo.fn_GetIdent('TestTable'), 'o'),
(dbo.fn_GetIdent('TestTable'), 'r'),
(dbo.fn_GetIdent('TestTable'), 'l'),
(dbo.fn_GetIdent('TestTable'), 'd')
INSERT INTO TestTable (New, Letter)
VALUES (dbo.fn_GetIdent('TestTable'), '!')
SELECT * FROM TestTable
/*
Result
ID New Letter
1 1 H
2 2 e
3 3 l
4 4 l
5 5 o
6 6
7 7 W
8 8 o
9 9 r
10 10 l
11 11 d
12 12 !
*/

SQL Server trigger that rounds values

I need to write a trigger that rounds a value whenever it is inserted/updated into a table
Here is a getting started tutorial on how to use Triggers in SQL Server. Start working on it and let us know if you have any issues.
DECLARE #tt TABLE (id INT NOT NULL PRIMARY KEY, value NUMERIC(10, 0) NOT NULL)
INSERT
INTO #tt
VALUES (1, 3.1415926)
SELECT *
FROM #tt
, without any triggers.