Check if field exists in DB using SQL trigger - sql

I have a database Users that has four fields: Name, Client, ID, Time. Client is an integer (0-99). How to write a trigger that will find latest user from Users (latest according to Time) during Insert and if the Client of this user equals Client of inserted user then I'd like to Rollback
I tried like this:
CREATE TRIGGER DoubledData ON Users
FOR INSERT
AS
DECLARE #client DECIMAL(2)
DECLARE #client_old DECIMAL(2)
DECLARE #name Varchar(50)
SELECT #name = Name from inserted
SELECT #client = Client from inserted
//This doesn't work, "Syntax error near Select":
SELECT #client_old = Select top(1) Client from Users where Name like #name order by Time desc;
IF #client = #client_old
BEGIN
ROLLBACK
END
The problem is that I can assign same values to Client for one user but they can't be one after another (eg for client this order is correct 1-2-3-1-3 -> order is important, but this isn't correct: 1-2-3-3 -> after 2nd occurrence of '3' in a row it needs to be rollbacked)
I'm using MS SQL
[EDIT]
I have found that I can execute it without Select top(1) like:
SELECT #client_old = Client from Users where Name like #name order by Time desc;
But the trigger doesn't execute afer insert

First, you clearly don't understand triggers in SQL Server and the inserted pseudo-tables. These can have more than one row, so your code will fail when multiple rows are inserted. Sadly, there is no compile check for this situation. And code can unexpectedly fail (even in production, alas).
Second, the right way to do this is probably with a unique constraint. That would be:
alter table users
add constraint unq_users_name_client unique (name, client);
This would ensure no duplication, so it is a stronger condition than your trigger.

Related

Using BizTalk 2013r2 to UPSERT via WCF-SQL stored procedure

I'm currently trying to write a canonical schema to multiple related tables within a SQL DB, but I'm experience DUPLICATE KEY ID conflicts when it's evaluating whether the record exists prior to UPDATING/INSERTING.
BizTalk receives change records from the student management system every 5 minutes, maps them to a stored procedure and then calls that procedure which writes the changes to 5 tables in our master database.
I believe this is because I'm using an incorrect design pattern in the stored procedure.
Current Design:
IF EXISTS (Select student_id FROM student_modules WHERE student_id #student_id and module_id = #module_id)
-- THEN UPDATE THE RECORD
ELSE
-- INSERT THE RECORD
Logically this makes sense, but as BizTalk receives 2 change records with the exact same student and module ID at the same time, and then attempts to call the stored procedure for each record.
SQL then panics, because whilst it's evaluating the logic in the first message, it tries to execute the INSERT whilst evaluating the same logic in the second message - and tells me I'm trying to insert a DUPLICATE KEY.
I've tried using an UPSERT pattern that i found at the below link (design below), but that seems to lock the student_modules table completely.
BEGIN TRANSACTION;
UPDATE dbo.t WITH (UPDLOCK, SERIALIZABLE) SET val = #val WHERE [key] = #key;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.t([key], val) VALUES(#key, #val);
END
COMMIT TRANSACTION;
https://sqlperformance.com/2020/09/locking/upsert-anti-pattern
Is there a cleaner approach to this that I'm missing?
You could use the MERGE Transact-SQL command
INSERT tbl_A (col, col2)
SELECT col, col2
FROM tbl_B
WHERE NOT EXISTS (SELECT col FROM tbl_A A2 WHERE A2.col = tbl_B.col);
You will also want to consider either changing your Orchestration so that it subscribes to further updates for the same student ID (a singleton type pattern) or to set your send port to ordered delivery, to prevent trying to update the same record at the same time.

Track logins sql

I have a job that runs on my server to track the last login on my sql server so I can audit inactive users.
First I enabled track successful logins on the server
I created a table called TRACK_LOGIN and run this daily:
INSERT INTO dbadb.dbo.TRACK_LOGIN (logontime, logon, loginname) EXEC XP_READERRORLOG 0, 1, [LOGIN SUCCEEDED FOR USER]
Now that that information is in the TRACK_LOGIN table I query DISTINCT out of that table and put it in another table with this query:
SELECT DISTINCT SUBSTRING(LOGINNAME,PATINDEX('%''%',LOGINNAME)+1,PATINDEX('%.%',LOGINNAME)-PATINDEX('%''%',LOGINNAME))FROM TRACK_LOGIN
I would also like to query the column logontime along with the distinct login so I have a list daily of who logs in and what time they login?
Please help modify the select statement above to include distinct logins along with their last logontime.
This is intended on allowing me to look back at my users last login and eliminate those on the server that are not used.
I understand that you have already put some real effort into make this work, but I would still suggest to go with a different approach that yields a much cleaner result:
Logon triggers
This will allow you to insert the right type of data into your table and will not force you to parse back log entries.
This example here shows a different use case, but I think you will have no issue to port it to your own problem.
CREATE TRIGGER MyLogonTrigger ON ALL SERVER FOR LOGON
AS
BEGIN
IF SUSER_SNAME() <> 'sa'
INSERT INTO Test.dbo.LogonAudit (UserName, LogonDate, spid)
VALUES (SUSER_SNAME(), GETDATE(), ##SPID);
END;
GO
ENABLE TRIGGER MyLogonTrigger ON ALL SERVER;
Ok to track logins I did this, I abounded the first method and implemented this:
First I created a table called logonaudit:
CREATE TABLE LogonAudit
(
AuditID INT NOT NULL CONSTRAINT PK_LogonAudit_AuditID
PRIMARY KEY CLUSTERED IDENTITY(1,1)
, UserName NVARCHAR(255)
, LogonDate DATETIME
, spid INT NOT NULL
);
I then had to grant insert on that table:
GRANT INSERT ON dbadb.dbo.LogonAudit TO public;
I created another table called auditloginresults:
create table auditLoginResults
(
AuditID INT,
Username NVARCHAR(255),
LogonDate DATETIME,
SPID INT
);
I then created a trigger to log all logins and times to the first table LogonAudit. I had to create a logon called login_audit and allow it to insert into my tables. I then had to use the origional_login() to log the users login, if you dont do this it will block all logins that are not sa
CREATE TRIGGER MyLogonTrigger
ON ALL SERVER WITH EXECUTE AS 'login_audit'
FOR LOGON
AS
BEGIN
INSERT INTO DBADb.dbo.LogonAudit (UserName, LogonDate, spid)
VALUES (ORIGINAL_LOGIN(), GETDATE(), ##SPID);
END;
Now I created a job (you will need to create a job to run at a specific time with this code, This is not the code for the job just the code you would run in your job) to query the first table LogonAudit and put the results into the auditloginResults table, after that step I cleaned out the first table LogonAudit by running another step to delete data in the first table. Im not going to post the job to keep the threat clean but here is what is run in the job
The create job step 1--------------------------------------------------------
INSERT INTO DBADb.dbo.auditLoginResults
SELECT I.*
FROM DBADb.[dbo].[LogonAudit] AS I
INNER JOIN
(SELECT UserName, MAX([logondate]) AS MaxDate
FROM DBADb.[dbo].[LogonAudit]
GROUP BY UserName
) AS M ON I.logondate = M.MaxDate
AND I.UserName = M.UserName
`
-----NOW create job to purge the logonaudit table step 2
DELETE FROM dbadb.dbo.auditLoginResults;
-----now create a stored procedure to execute this will query the auditloginreaults and provide you the last login of everyone that has ever logged into the database
SELECT I.*
FROM DBADb.[dbo].[auditLoginResults] AS I
INNER JOIN
(SELECT UserName, MAX([logondate]) AS MaxDate
FROM DBADb.[dbo].[ auditLoginResults]
GROUP BY UserName
) AS M ON I.logondate = M.MaxDate
AND I.UserName = M.UserName

SQL Table Locking

I have an SQL Server locking question regarding an application we have in house. The application takes submissions of data and persists them into an SQL Server table. Each submission is also assigned a special catalog number (unrelated to the identity field in the table) which is a sequential alpha numeric number. These numbers are pulled from another table and are not generated at run time. So the steps are
Insert Data into Submission Table
Grab next Unassigned Catalog
Number from Catalog Table
Assign the Catalog Number to the
Submission in the Submission table
All these steps happen sequentially in the same stored procedure.
Its, rate but sometimes we manage to get two submission at the same second and they both get assigned the same Catalog Number which causes a localized version of the Apocalypse in our company for a small while.
What can we do to limit the over assignment of the catalog numbers?
When getting your next catalog number, use row locking to protect the time between you finding it and marking it as in use, e.g.:
set transaction isolation level REPEATABLE READ
begin transaction
select top 1 #catalog_number = catalog_number
from catalog_numbers with (updlock,rowlock)
where assigned = 0
update catalog_numbers set assigned = 1 where catalog_number = :catalog_number
commit transaction
You could use an identity field to produce the catalog numbers, that way you can safely create and get the number:
insert into Catalog () values ()
set #CatalogNumber = scope_identity()
The scope_identity function will return the id of the last record created in the same session, so separate sessions can create records at the same time and still end up with the correct id.
If you can't use an identity field to create the catalog numbers, you have to use a transaction to make sure that you can determine the next number and create it without another session accessing the table.
I like araqnid's response. You could also use an insert trigger on the submission table to accomplish this. The trigger would be in the scope of the insert, and you would effectively embed the logic to assign the catalog_number in the trigger. Just wanted to put your options up here.
Here's the easy solution. No race condition. No blocking from a restrictive transaction isolation level. Probably won't work in SQL dialects other than T-SQL, though.
I assume their is some outside force at work to keep your catalog number table populated with unassigned catalog numbers.
This technique should work for you: just do the same sort of "interlocked update" that retrieves a value, something like:
update top 1 CatalogNumber
set in_use = 1 ,
#newCatalogNumber = catalog_number
from CatalogNumber
where in_use = 0
Anyway, the following stored procedure just just ticks up a number on each execution and hands back the previous one. If you want fancier value, add a computed column that applies the transform of choice to the incrementing value to get the desired value.
drop table dbo.PrimaryKeyGenerator
go
create table dbo.PrimaryKeyGenerator
(
id varchar(100) not null ,
current_value int not null default(1) ,
constraint PrimaryKeyGenerator_PK primary key clustered ( id ) ,
)
go
drop procedure dbo.GetNewPrimaryKey
go
create procedure dbo.GetNewPrimaryKey
#name varchar(100)
as
set nocount on
set ansi_nulls on
set concat_null_yields_null on
set xact_abort on
declare
#uniqueValue int
--
-- put the supplied key in canonical form
--
set #name = ltrim(rtrim(lower(#name)))
--
-- if the name isn't already defined in the table, define it.
--
insert dbo.PrimaryKeyGenerator ( id )
select id = #name
where not exists ( select *
from dbo.PrimaryKeyGenerator pkg
where pkg.id = #name
)
--
-- now, an interlocked update to get the current value and increment the table
--
update PrimaryKeyGenerator
set #uniqueValue = current_value ,
current_value = current_value + 1
where id = #name
--
-- return the new unique value to the caller
--
return #uniqueValue
go
To use it:
declare #pk int
exec #pk = dbo.GetNewPrimaryKey 'foobar'
select #pk
Trivial to mod it to return a result set or return the value via an OUTPUT parameter.

Is my stored procedure executing out of order?

Brief history:
I'm writing a stored procedure to support a legacy reporting system (using SQL Server Reporting Services 2000) on a legacy web application.
In keeping with the original implementation style, each report has a dedicated stored procedure in the database that performs all the querying necessary to return a "final" dataset that can be rendered simply by the report server.
Due to the business requirements of this report, the returned dataset has an unknown number of columns (it depends on the user who executes the report, but may have 4-30 columns).
Throughout the stored procedure, I keep a column UserID to track the user's ID to perform additional querying. At the end, however, I do something like this:
UPDATE #result
SET Name = ppl.LastName + ', ' + ppl.FirstName
FROM #result r
LEFT JOIN Users u ON u.id = r.userID
LEFT JOIN People ppl ON ppl.id = u.PersonID
ALTER TABLE #result
DROP COLUMN [UserID]
SELECT * FROM #result r ORDER BY Name
Effectively I set the Name varchar column (that was previously left NULL while I was performing some pivot logic) to the desired name format in plain text.
When finished, I want to drop the UserID column as the report user shouldn't see this.
Finally, the data set returned has one column for the username, and an arbitrary number of INT columns with performance totals. For this reason, I can't simply exclude the UserID column since SQL doesn't support "SELECT * EXCEPT [UserID]" or the like.
With this known (any style pointers are appreciated but not central to this problem), here's the problem:
When I execute this stored procedure, I get an execution error:
Invalid column name 'userID'.
However, if I comment out my DROP COLUMN statement and retain the UserID, the stored procedure performs correctly.
What's going on? It certainly looks like the statements are executing out of order and it's dropping the column before I can use it to set the name strings!
[Edit 1]
I defined UserID previously (the whole stored procedure is about 200 lies of mostly irrelevant logic, so I'll paste snippets:
CREATE TABLE #result ([Name] NVARCHAR(256), [UserID] INT);
Case sensitivity isn't the problem but did point me to the right line - there was one place in which I had userID instead of UserID. Now that I fixed the case, the error message complains about UserID.
My "broken" stored procedure also works properly in SQL Server 2008 - this is either a 2000 bug or I'm severely misunderstanding how SQL Server used to work.
Thanks everyone for chiming in!
For anyone searching this in the future, I've added an extremely crude workaround to be 2000-compatible until we update our production version:
DECLARE #workaroundTableName NVARCHAR(256), #workaroundQuery NVARCHAR(2000)
SET #workaroundQuery = 'SELECT [Name]';
DECLARE cur_workaround CURSOR FOR
SELECT COLUMN_NAME FROM [tempdb].INFORMATION_SCHEMA.Columns WHERE TABLE_NAME LIKE '#result%' AND COLUMN_NAME <> 'UserID'
OPEN cur_workaround;
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #workaroundQuery = #workaroundQuery + ',[' + #workaroundTableName + ']'
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
END
CLOSE cur_workaround;
DEALLOCATE cur_workaround;
SET #workaroundQuery = #workaroundQuery + ' FROM #result ORDER BY Name ASC'
EXEC(#workaroundQuery);
Thanks everyone!
A much easier solution would be to not drop the column, but don't return it in the final select.
There are all sorts of reasons why you shouldn't be returning select * from your procedure anyway.
EDIT: I see now that you have to do it this way because of an unknown number of columns.
Based on the error message, is the database case sensitive, and so there's a difference between userID and UserID?
This works for me:
CREATE TABLE #temp_t
(
myInt int,
myUser varchar(100)
)
INSERT INTO #temp_t(myInt, myUser) VALUES(1, 'Jon1')
INSERT INTO #temp_t(myInt, myUser) VALUES(2, 'Jon2')
INSERT INTO #temp_t(myInt, myUser) VALUES(3, 'Jon3')
INSERT INTO #temp_t(myInt, myUser) VALUES(4, 'Jon4')
ALTER TABLE #temp_t
DROP Column myUser
SELECT * FROM #temp_t
DROP TABLE #temp_t
It says invalid column for you. Did you check the spelling and ensure there even exists that column in your temp table.
You might try wrapping everything preceding the DROP COLUMN in a BEGIN...COMMIT transaction.
At compile time, SQL Server is probably expanding the * into the full list of columns. Thus, at run time, SQL Server executes "SELECT UserID, Name, LastName, FirstName, ..." instead of "SELECT *". Dynamically assembling the final SELECT into a string and then EXECing it at the end of the stored procedure may be the way to go.

atomic compare and swap in a database

I am working on a work queueing solution. I want to query a given row in the database, where a status column has a specific value, modify that value and return the row, and I want to do it atomically, so that no other query will see it:
begin transaction
select * from table where pk = x and status = y
update table set status = z where pk = x
commit transaction
--(the row would be returned)
it must be impossible for 2 or more concurrent queries to return the row (one query execution would see the row while its status = y) -- sort of like an interlocked CompareAndExchange operation.
I know the code above runs (for SQL server), but will the swap always be atomic?
I need a solution that will work for SQL Server and Oracle
Is PK the primary key? Then this is a non issue, if you already know the primary key there is no sport. If pk is the primary key, then this begs the obvious question how do you know the pk of the item to dequeue...
The problem is if you don't know the primary key and want to dequeue the next 'available' (ie. status = y) and mark it as dequeued (delete it or set status = z).
The proper way to do this is to use a single statement. Unfortunately the syntax differs between Oracle and SQL Server. The SQL Server syntax is:
update top (1) [<table>]
set status = z
output DELETED.*
where status = y;
I'm not familiar enough with Oracle's RETURNING clause to give an example similar to SQL's OUTPUT one.
Other SQL Server solutions require lock hints on the SELECT (with UPDLOCK) to be correct.
In Oracle the preffered avenue is use the FOR UPDATE, but that does not work in SQL Server since FOR UPDATE is to be used in conjunction with cursors in SQL.
In any case, the behavior you have in the original post is incorrect. Multiple sessions can all select the same row(s) and even all update it, returning the same dequeued item(s) to multiple readers.
As a general rule, to make an operation like this atomic you'll need to ensure that you set an exclusive (or update) lock when you perform the select so that no other transaction can read the row before your update.
The typical syntax for this is something like:
select * from table where pk = x and status = y for update
but you'd need to look it up to be sure.
I have some applications that follow a similar pattern. There is a table like yours that represents a queue of work. The table has two extra columns: thread_id and thread_date. When the app asks for work froom the queue, it submits a thread id. Then a single update statement updates all applicable rows with the thread id column with the submitted id and the thread date column with the current time. After that update, it selects all rows with that thread id. This way you dont need to declare an explicit transaction. The "locking" occurs in the initial update.
The thread_date column is used to ensure that you do not end up with orphaned work items. What happens if items are pulled from the queue and then your app crashes? You have to have the ability to try those work items again. So you might grab all items off the queue that have not been marked completed but have been assigned to a thread with a thread date in the distant past. Its up to you to define "distant."
Try this. The validation is in the UPDATE statement.
Code
IF EXISTS (SELECT * FROM sys.tables WHERE name = 't1')
DROP TABLE dbo.t1
GO
CREATE TABLE dbo.t1 (
ColID int IDENTITY,
[Status] varchar(20)
)
GO
DECLARE #id int
DECLARE #initialValue varchar(20)
DECLARE #newValue varchar(20)
SET #initialValue = 'Initial Value'
INSERT INTO dbo.t1 (Status) VALUES (#initialValue)
SELECT #id = SCOPE_IDENTITY()
SET #newValue = 'Updated Value'
BEGIN TRAN
UPDATE dbo.t1
SET
#initialValue = [Status],
[Status] = #newValue
WHERE ColID = #id
AND [Status] = #initialValue
SELECT ColID, [Status] FROM dbo.t1
COMMIT TRAN
SELECT #initialValue AS '#initialValue', #newValue AS '#newValue'
Results
ColID Status
----- -------------
1 Updated Value
#initialValue #newValue
------------- -------------
Initial Value Updated Value