Atomically INSERT+SELECT or SELECT row (based on UNIQUE column) - sql

My table is created as follows:
CREATE TABLE StackFunctionID
(
m_FunctionID int PRIMARY KEY IDENTITY,
m_GroupID int DEFAULT 0 NOT NULL,
m_Function varchar(256) UNIQUE NOT NULL,
);
GO
CREATE TRIGGER INSERT_MakeGroupID ON StackFunctionID AFTER INSERT AS
BEGIN
SET NOCOUNT ON;
UPDATE StackFunctionID
SET StackFunctionID.m_GroupID = INSERTED.m_FunctionID
FROM StackFunctionID
INNER JOIN INSERTED ON (StackFunctionID.m_FunctionID = INSERTED.m_FunctionID)
END;
Now I'm trying to write a query for Sql Server 2008 that will
If m_Function does not exist, create it, and return m_FunctionID, m_GroupID
If m_Function exists, return m_FunctionID, m_GroupID
Perform it atomically
After much trying, this is what worked for the last years:
DECLARE #Results TABLE(m_FunctionID INT, m_GroupID INT); -- trash use
DECLARE #dummy1 int; -- trash use
MERGE INTO StackFunctionID WITH(HOLDLOCK) AS Target -- WITH(HOLDLOCK) for thread safety
USING (VALUES(null)) AS dummy2(dummy3) -- syntax requires USING
ON Target.m_Function = #m_Function
WHEN MATCHED THEN
UPDATE SET #dummy1 = 0 -- syntax requires something in 'WHEN MATCHED', which is needed for OUTPUT
WHEN NOT MATCHED THEN
INSERT(m_Function) VALUES(#m_Function)
OUTPUT inserted.m_FunctionID, inserted.m_GroupID
INTO #Results; -- Having a trigger requires 'INTO'
SELECT * FROM #Results -- Having a trigger requires 'INTO'
However, there was a bug: when a row is INSERTED, OUTPUT returns m_GroupID=0
Questions:
How do I always get correct m_GroupID ?
Is it possible to achieve the goal with a simpler query? (because current one is really hard to understand with all those "syntax requirements")

Relying on UNIQUE m_Function use TRY CATCH to INSERT and then SELECT.
Demo
CREATE TABLE StackFunctionID
(
m_FunctionID int PRIMARY KEY IDENTITY,
m_GroupID int DEFAULT 0 NOT NULL,
m_Function varchar(256) UNIQUE NOT NULL,
);
INSERT StackFunctionID(m_Function)
VALUES ('a'), ('b');
SELECT * FROM StackFunctionID;
DECLARE #m_Function varchar(256) = 'a';
BEGIN TRY
INSERT StackFunctionID(m_Function)
VALUES ( #m_Function);
END TRY
BEGIN CATCH
PRINT 'Already exists';
END CATCH
SELECT m_FunctionID, m_GroupID
FROM StackFunctionID
WHERE m_Function = #m_Function;

Related

Trigger on View not firing on SQL Server 2012

I have seen some articles mention the possibility of a Trigger on a View, triggering on either insert, updates or deletes to one of the base tables from which the View is created.
However I am not able to get a simple example to work.
CREATE TABLE [Test].[Data] (
Id INT PRIMARY KEY IDENTITY (1,1),
Data VARCHAR(255) NOT NULL,
);
GO
CREATE VIEW [Test].[View] AS SELECT * FROM [Test].[Data];
GO
CREATE TABLE [Test].[Queue] (
Id INT PRIMARY KEY IDENTITY (1,1),
DataId INT NOT NULL,
Action VARCHAR(255) NOT NULL,
Timestamp DATETIME NOT NULL,
);
GO
CREATE TRIGGER InsertTrigger ON [Test].[View] INSTEAD OF INSERT AS
BEGIN
DECLARE #DataId INT;
DECLARE #Timestamp DATETIME;
SET #DataId = (SELECT Id FROM INSERTED);
SET #Timestamp = GETDATE();
INSERT INTO [Test].[Queue] (DataId, Action, Timestamp) VALUES (#DataId, 'Insert', #Timestamp)
END
GO
ENABLE TRIGGER InsertTrigger ON [Test].[View];
GO
INSERT INTO [Test].[Data] (Data) VALUES ('Testdata');
The trigger is not firing, is the above not possible or is there something wrong with my Sql?
Edit: Although answered I would like to clarify the question. The idea was to get the trigger on the View to fire, when there was an Insert to the base table and not the View itself.
A trigger on a view will only work on inserts into that view, not on any inserts into tables to which the view references.
In your script you're not inserting into that view, you're inserting into a table.
In addition to not testing this correctly, your view is wrong. You are not considering that inserted represents multiple rows, not one.
So:
CREATE TRIGGER InsertTrigger ON [Test].[View] INSTEAD OF INSERT AS
BEGIN
INSERT INTO [Test].[Queue] (DataId, Action, Timestamp)
SELECT i.Id, 'Insert', GETDATE()
FROM Inserted;
END;
GO
INSERT INTO [Test].[View] (Data)
VALUES ('Testdata');

Error during trigger execution: Error converting data type nvarchar to bigint

I have created a trigger which must update the total amount from Account table. Whenever some data is update from Sale table, the trigger executes a store procedure calculating the current amount and inserting it into Account, but when it's about to update the Account table, some quite strange error occurs:
The data in row 5 was not committed Error Source: .Net SqlClient
DataProvider Error Message: Error converting data type nvarchar to
bigint. The statement have been terminated.
Below there is the Sale's trigger script:
CREATE TRIGGER [dbo].[Trigger_Sale]
ON [dbo].[Sale]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
exec ComputeAccountAmount ID_Account
END
And the procedure ComputeAccountAmount:
CREATE PROCEDURE [dbo].[ComputeAccountAmount]
#IdAccount bigint
AS
begin transaction
update Account set AccountAmount = (SELECT sum(AmountSold)
from Sale
where #IdAccount = ID_Account)
where #IdAccount = ID_Account
commit
I've already checked out all the types the procedures uses, yet its tables and everything is bigint as shown below:
CREATE TABLE [dbo].[Account] (
[ID_Account] BIGINT IDENTITY (1, 1) NOT NULL,
[ExpireDate] DATE NOT NULL,
[PurchaseLimit] MONEY NOT NULL,
[OpeningDate] DATE NOT NULL,
[ID_Customer] INT NOT NULL,
[AccountAmount] MONEY NULL,
CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED ([ID_Account] ASC),
CONSTRAINT [FK_Account_Customer] FOREIGN KEY ([ID_Customer]) REFERENCES [dbo].[Customer] ([ID_Customer]) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE [dbo].[Sale] (
[ID_Sale] INT IDENTITY (1, 1) NOT NULL,
[SaleDate] DATE NOT NULL,
[AmountSold] MONEY NOT NULL,
[ID_Account] BIGINT NULL,
PRIMARY KEY CLUSTERED ([ID_Account] ASC)
);
For testing, I'm using the Visual Studio to manually verify the trigger. What's going on?
Obviously you have syntax error in your trigger
CREATE TRIGGER [dbo].[Trigger_Sale]
ON [dbo].[Sale]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
exec ComputeAccountAmount ID_Account
END
What is ID_Account? It will throw an error
You need to select distinct accountIDs from INSERTED and DELETED tables in your trigger and for each of this account call exec ComputeAccountAmount. Something like:
CREATE TRIGGER [dbo].[Trigger_Sale] ON [dbo].[Sale]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
DECLARE #AccountID BIGINT
DECLARE trCur CURSOR FAST_FORWARD READ_ONLY
FOR
SELECT AccountID FROM DELETED
UNION
SELECT AccountID FROM INSERTED
OPEN trCur
FETCH NEXT FROM trCur INTO #AccountID
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC ComputeAccountAmount #AccountID
FETCH NEXT FROM trCur INTO #AccountID
END
CLOSE trCur
DEALLOCATE trCur
END

Supposedly easy trigger

I've created a trigger which is to block inserted records with a date already existing in a table.
CREATE TRIGGER [dbo].[SpecialOffers_Insert]
ON [dbo].[SpecialOffers]
FOR INSERT,UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT * FROM inserted WHERE SPO_DateFrom IN (SELECT SPO_DateFrom FROM dbo.SpecialOffers))
BEGIN
RAISERROR('Error. ', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
It is added to a table:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL)
The table is empty but when trying to insert such record:
INSERT INTO dbo.SpecialOffers (SPO_DateFrom, SPO_DateTo) VALUES ('2015-01-15','2015-01-15')
I got the Error from the trigger. How should I modify the trigger not to get the error?
If the goal is to block inserted records with date already existing in a table, you don't need a trigger - just create a unique constraint on the date field:
ALTER TABLE [dbo].[SpecialOffers]
ADD CONSTRAINT SpecialOffersUQ UNIQUE (SPO_DateFrom)
If you wanted a trigger to prevent overlaps, why didn't you say so:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL,
constraint CK_SO_NoTimeTravel CHECK (SPO_DateFrom <= SPO_DateTo)
)
GO
CREATE TRIGGER NoOverlaps
on dbo.SpecialOffers
after insert,update
as
set nocount on
if exists (
select *
from dbo.SpecialOffers so1
inner join
dbo.SpecialOffers so2
on
so1.SPO_DateFrom < so2.SPO_DateTo and
so2.SPO_DateFrom < so1.SPO_DateTo and
so1.SPO_SpoId != so2.SPO_SpoId
inner join
inserted i
on
so1.SPO_SpoId = i.SPO_SpoId
)
begin
RAISERROR('No overlaps',16,1)
ROLLBACK
end
Examples:
--Works
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Constraint)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20011231','20010101')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20020101','20021231'),
('20020701','20030630')
I also added a check constraint so that I didn't have to deal with nonsense data in the trigger.
You might have to change swap some of the <s for <=s or vice-versa, depending on what definition of intervals you want to use (i.e. are DateFrom and DateTo meant to be inclusive or exclusive endpoints for the interval they're describing?)
Since the trigger runs in the transaction context of the SQL statement that fired it, after this INSERT, there will be a row in your table dbo.SpecialOffers with the SPO_DateFrom values you've just inserted and the SELECT from the table will succeed ...
Therefore, the trigger will assume that there's already been a value - and it throws the error (as designed).
You could rewrite the trigger to not look at the newly inserted rows, but anything else - but as others have pointed out, a UNIQUE constraint does that much more simply
You should check if the rows you found are actually NOT the ones you have just inserted. Change the line
IF EXISTS (
SELECT * FROM inserted
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers)
)
To
IF EXISTS (
SELECT * FROM inserted a
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers b
WHERE a.SPO_SpoId <> b.SPO_SpoId)
)

Deletion\Creation of Temp tables in SQL Server 2008

I have SQL code like this
IF Object_id('tempdb..#empDate) IS NOT NULL
DROP TABLE #empDate
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
After the above code some more lines of SQL follow and then it is repeated.
I get the following error.
Msg 2714, Level 16, State 1, Line 589
There is already an object named '#empDate' in the database.
I replaced the
IF Object_id('tempdb..#empDate) IS NOT NULL
with
IF Object_id('tempdb..#empDate%) IS NOT NULL
As it is written on the forums that SQL Server appends number to the subsequent temp table(s).
Source:
Check if a temporary table exists and delete if it exists before creating a temporary table
http://blog.sqlauthority.com/2009/05/17/sql-server-how-to-drop-temp-table-check-existence-of-temp-table/
http://blog.sqlauthority.com/2009/03/29/sql-server-fix-error-msg-2714-level-16-state-6-there-is-already-an-object-named-temp-in-the-database/
I am using Microsoft SQL Server 2008 on Windows 7 Enterprise.
I am not able to understand the cause of the error.
Please help.
Sample One
This will fail......
Executing the same code again, will throw the error you are getting now
IF Object_id('tempdb..#empDate') IS NOT NULL
BEGIN
DROP TABLE #empDate
END
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
IF Object_id('tempdb..#empDate') IS NOT NULL
BEGIN
DROP TABLE #empDate
END
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
Sample Two (Fixed)
IF Object_id('tempdb..#empDate') IS NOT NULL
BEGIN
DROP TABLE #empDate
END
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
GO --<-- Adding this Batch Separator will eliminate the Error
IF Object_id('tempdb..#empDate') IS NOT NULL
BEGIN
DROP TABLE #empDate
END
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
Test
If you try Executing the following Statements in ONE BATCH they will fail even though there isnt any table at all with the name #empDate, it will not even execute the very 1st Create table Statement. and will throw an error.
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
DROP TABLE #empDate
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
But if you separate all the statement in separate batches they will be executed successfully something like this..
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
GO
DROP TABLE #empDate
GO
CREATE TABLE #empDate
(
[empID] INT,
[AddLoc] VARCHAR(1000)
)
GO
I would just drop your table without any pre-checks.
Then write/run the script clean.
Once done using the temp table, drop it at the end of your script.
So run this unconditionally
DROP TABLE #empDate
Then write/run your script and make sure you have this line at the end of your script.
pass database name with object_id
example :
DECLARE #db_id int;
DECLARE #object_id int;
SET #db_id = DB_ID(N'AdventureWorks2012');
SET #object_id = OBJECT_ID(N'AdventureWorks2012.Person.Address');
IF #db_id IS NULL
BEGIN;
PRINT N'Invalid database';
END;
ELSE IF #object_id IS NULL
BEGIN;
PRINT N'Invalid object';
END;
ELSE
BEGIN;
SELECT * FROM sys.dm_db_index_operational_stats(#db_id, #object_id, NULL, NULL);
END;
GO

Finding what value was inserted into an auto-incrementing field

I have two tables:
Rooms:
ID (auto-incrementing primary key, int)
Topic (varchar(50))
MangerId (varchar(50))
Rooms_Users:
UserId (varchar(50))
RoomId (varchar(50))
both fields together are the primary key
I want to insert a room but I also must insert the manger to the table rooms_users.
Here is what I have so far:
ALTER PROCEDURE [dbo].[Creat_Room] #MangerId varchar(50) ,#Topic varchar(50)
AS
BEGIN
SET NOCOUNT
insert into Rooms(ManagerId,Topic) values(#MangerId,#Topic)
insert into Rooms_Users(UserId,RoomId) values(#MangerId,?????????????)
END
The ????????????? is the problem: I don't know what to put here i want to put the roomid i insert above.
You can use the output clause. Look at MSDN here: OUTPUT Clause (Transact-SQL)
Example:
declare #tbl table
(
NewID int
)
insert into Rooms(ManagerId,Topic)
output inserted.ID into #tbl
values(#MangerId,#Topic)
Then the table variable will contains the new id given to the row you inserted
Use the SCOPE_IDENTITY() function:
ALTER PROCEDURE [dbo].[Create_Room]
#ManagerId varchar(50),
#Topic varchar(50)
AS
BEGIN
DECLARE #NewRoomID INT
insert into Rooms(ManagerId, Topic) values(#MangerId, #Topic)
SELECT #NewRoomID = SCOPE_IDENTITY()
insert into Rooms_Users(UserId, RoomId) values(#ManagerId, #NewRoomID)
END
This function will return the last inserted IDENTITY value in this particular scope - the scope of your stored procedure.