Insert record in table with restriction - sql-server

I use sql server and a table like this:
MyTbale(ID int IDENTITY, Type Char(1), ParentId int)
I want to restrict my users that insert the first record with some ParentId with Type = 'O', and can insert other record with some parentId with each Type. In other word if user want to insert a record with ParentId=10 must set Type = 'O' and after this, can Insert each type with ParentId = 10.
Sample Data:
Id Type ParentId
1 O 10
2 I 10
3 I 10
4 M 10
5 N 10
6 O 12
7 M 12
8 I 12
What's the best solution for this problem? ( I think that I can use constraint with UDF and trigger for this purpose).
EDIT
by following format I can add check constraint:
Create FUNCTION [dbo].[f](#Id INT, #Date DATE) RETURNS BIT
AS BEGIN
DECLARE #R BIT = CASE WHEN EXISTS(SELECT Date FROM MyTable WHERE Date=#Date AND Id!=#Id) THEN 1 ELSE 0 END
RETURN #R
END
GO
ALTER TABLE [dbo].[MyTable] WITH CHECK ADD CONSTRAINT [CK_test] CHECK (([dbo].[F]([Id],[Date])=(0)))
and by following query I can add trigger for check data:
CREATE TRIGGER dbo.tr_MyTable ON dbo.MyTable
INSTEAD OF INSERT
AS BEGIN
IF EXISTS(SELECT *
FROM Inserted i
LEFT JOIN MyTable t ON t.ParentId = i.ParentId AND t.Type = 'O'
WHERE i.Type <> 'O'
AND t.Id IS NULL) BEGIN
RAISERROR('Type must be equal to O in first row',16,1)
END
INSERT INTO dbo.MyTable(Type,ParentId)
SELECT Type, ParentId
FROM Inserted
END

The INSTEAD OF TRIGGER would be the way to go. Also, if you don't need the error information returned, you might gain some performance with this slightly revised suggestion:
CREATE TRIGGER dbo.tr_MyTable ON dbo.MyTable
INSTEAD OF INSERT
AS BEGIN
INSERT INTO dbo.MyTable(Type,ParentId)
SELECT Type, ParentId
FROM Inserted
LEFT JOIN MyTable t ON t.ParentId = i.ParentId AND t.Type = 'O'
WHERE i.Type <> 'O'
AND t.Id IS NULL

Related

How can I update a column in SQL Server using a trigger

I am trying to create a trigger within SQL Server Management Studio that will increment a column value by 1 when a separate column has been updated within the same table.
The value for the column we want to update when the update script has been ran becomes NULL
My example is that I when I change the address of a customer, I want a column that goes up by 1 every time the address is changed i.e NoOfAddressess = 1, 2, 3 etc...
Here is the SQL code that I am writing
ALTER TRIGGER trg_customeraudit
ON tblCustomer
AFTER UPDATE, DELETE, INSERT
AS
INSERT INTO dbo.CustomerDetailsAudit
VALUES (CURRENT_USER, CURRENT_TIMESTAMP,
(SELECT CustomerID FROM inserted),
(SELECT CustomerAddress FROM deleted),
(SELECT CustomerAddress FROM inserted),
(SELECT CustomerPostcode FROM deleted),
(SELECT CustomerPostcode FROM inserted),
(SELECT NumberOfChangedAddresses FROM dbo.CustomerDetailsAudit)
)
IF ((SELECT CustomerAddress FROM inserted) =
(SELECT CustomerAddress FROM deleted) OR
(SELECT CustomerPostcode FROM deleted) =
(SELECT CustomerPostcode FROM inserted))
BEGIN
RAISERROR ('You must enter both a new postcode and address',16,10)
ROLLBACK TRANSACTION
END
ELSE
BEGIN
PRINT 'Transaction successful'
WHERE CustomerID = (SELECT CustomerID from inserted)
END
IF UPDATE (CustomerName)
BEGIN
RAISERROR ('You cannot change the customer name', 16, 10)
ROLLBACK
END
Depending on the other things happening on this data triggers can be a very inefficient method of handling this, but here is one possible solution.
1. Setup
First create a table to use for testing.
create table test_table (
MyPrimaryKey int primary key clustered not null identity(1, 1)
, SomeColumn varchar(255) not null
, SomeColumnCounter int null
);
go
Now, add a trigger to initialize the counter to 1. This could be handled by a default constraint or set at the application level but it can also be done with a trigger.
-- this trigger will set the counter to 1 when a record is first added
-- doesn't need to be a trigger, but since the question was on triggers
create trigger trg_test_table_insert
on test_table
after insert
as
update tt
set tt.SomeColumnCounter = 1
from
test_table as tt
inner join
Inserted as i
on
tt.MyPrimaryKey = i.MyPrimaryKey;
go
Now, add a trigger that will check for changes on the designated column and increment the counter if needed.
-- this trigger will increment the counter by 1 if 'SomeColumn' changed
-- doesn't handle nulls so will need to be modified depending on schema
create trigger trg_test_table_update
on test_table
after update
as
update tt
set tt.SomeColumnCounter = tt.SomeColumnCounter + 1
from
Inserted as i -- new version of the record
inner join
Deleted as d -- old version of the record
on
i.MyPrimaryKey = d.MyPrimaryKey
and i.SomeColumn <> d.SomeColumn
inner join
test_table as tt
on
tt.MyPrimaryKey = i.MyPrimaryKey
go
2. Testing
Add some test data.
insert into test_table (SomeColumn)
values ('abc'), ('def');
go
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abc 1
2 def 1
Update without changing anything:
update tt
set tt.SomeColumn = 'abc'
from
test_table as tt
where
tt.MyPrimaryKey = 1
We still have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abc 1
2 def 1
Update that actually changes something:
update tt
set tt.SomeColumn = 'abbc'
from
test_table as tt
where
tt.MyPrimaryKey = 1
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abbc 2
2 def 1
Update that changes everything:
update tt
set tt.SomeColumn = tt.SomeColumn + 'z'
from
test_table as tt
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abbcz 3
2 defz 2

SQL - Check if there is data in a user defined type passed as an input to stored procedure

I have a stored proc with user defined type as an input as follows
//UDT
CREATE TYPE MY_UDT AS TABLE(
TYPE [varchar](20) NOT NULL
)
CREATE PROC PS
(
#UDT MY_UDT READONLY
)
AS
BEGIN
SELECT COL1, COL2...
FROM tblA
WHERE
1 = CASE WHEN /*CONDITION TO CHECK IF THE UDT DATA IS NULL*/ THEN 1
WHEN EXISTS(select 1 from #UDT WHERE TYPE = 'Red') AND COL2 IN
(SELECT TYPE FROM #UDT WHERE TYPE = 'Red') THEN 1
WHEN EXISTS (SELECT 1 FROM #UDT WHERE TYPE = 'Green') AND COL2
IN (SELECT TYPE FROM #UDT WHERE TYPE = 'Green') THEN 1
ELSE 0
END
END
how can we check if there is no data in UDT, to satisfy my first case condition, also in my second and third case conditions SELECT statement is written twice to filter results, is there a better way to not repeat the code? Thanks in advance
I have to agree this sounds a LOT like an xy problem. But pretty sure you could simplify this to something along these lines.
SELECT COL1, COL2...
FROM tblA a
left join #UDT udt on udt.TYPE = a.COL2 and udt.TYPE in ('Red', 'Green')
where udt.TYPE IS NOT NULL
OR NOT EXISTS(select * from #UDT)

NULL Values on query condition

I'm trying to compare null values using not in and exists but I found some diferences
Why the latest query returns null? is it related to a db parameter?
--SET ANSI_NULLS On
CREATE TABLE #Tmp (id INT)
CREATE TABLE #Tmp1 (id INT)
INSERT INTO #Tmp(id) VALUES (1)
INSERT INTO #Tmp(id) VALUES (null)
INSERT INTO #Tmp1(id) VALUES (1)
INSERT INTO #Tmp1(id) VALUES (null)
SELECT id FROM #Tmp WHERE id IN (SELECT id FROM #tmp1)
SELECT id FROM #Tmp WHERE EXISTS(SELECT 1 FROM #Tmp1 WHERE #Tmp1.id = #Tmp.id)
SELECT id FROM #Tmp WHERE id NOT IN (SELECT id FROM #tmp1)
SELECT id FROM #Tmp WHERE NOT EXISTS(SELECT 1 FROM #Tmp1 WHERE #Tmp1.id = #Tmp.id)
DROP TABLE #Tmp
DROP TABLE #Tmp1
In SQL Server, remember that NULL does not return an equality check of TRUE with anything (unless you are playing with ANSI_NULLS):
SELECT 1
WHERE 1=NULL
SELECT 1
WHERE NULL = NULL
It only returns True when you use IS/IS NOT
SELECT 1
WHERE NULL IS NULL
For your last query, the only record that satisfies that requirement in the WHERE clause is your NULL row. The equivalent is:
SELECT id FROM #Tmp
WHERE id IS NULL
To solve your issue, throw a second clause on there:
SELECT id FROM #Tmp
WHERE NOT EXISTS(
SELECT 1
FROM #Tmp1
WHERE #Tmp1.id = #Tmp.id OR #Tmp.id IS NULL
)
It is very simple:
id IN (1, NULL)
<=>
id = 1 OR id = NULL
And negation of alternative:
id NOT IN (1, NULL)
<=>
id != 1 AND id != NULL -- yields NULL
Summary:
If you want to use NOT IN make sure your list does not contain NULL.
SELECT id FROM #Tmp WHERE id NOT IN (SELECT id FROM #tmp1 WHERE id IS NOT NULL)
SELECT id FROM #Tmp WHERE id NOT IN (SELECT COALESCE(id,-1) FROM #tmp1)

Multiple select queries execution one after other

I am having six select queries with different where conditions if first select query returns null it should check the next select query and follows. what is the best approach to follow for writing it as stored procedure in SQL server.
You can use ##rowcount
DECLARE #OperatorID INT = 4, #CurrentCalendarView VARCHAR(50) = 'month';
declare #t table (operatorID int, CurrentCalendarView varchar(50));
insert into #t values (2, 'year');
select operatorID - 1, CurrentCalendarView from #t where 1 = 2
if (##ROWCOUNT = 0)
begin
select operatorID + 1, CurrentCalendarView from #t where 1 = 1
end
If I understand your question correctly then you can achieve this like below sample. You can go in this way.
if NOT EXISTS (SELECT TOP(1) 'x' FROM table WHERE id =#myId)
BEGIN
IF NOT EXISTS (SELECT TOP(1) 'x' FROM table2 WHERE id = #myId2)
BEGIN
IF NOT EXISTS (SELECT TOP(1) 'x' FROM table 3 WHERE id = #myID3)
BEGIN
END
END
END

SQL Server trigger on Insert and Update

I am looking to create a SQL Server trigger that moves a record from one table to an identical replica table if the record matches a specific condition.
Questions: do I need to specify each column, or can I use a wildcard?
Can I use something like:
SET #RecID = (SELECT [RecoID] FROM Inserted)
IF NULLIF(#RecID, '') IS NOT NULL
(then insert....)
THANKS!
There's a lot of stuff you "CAN" do in a trigger, but that doesn't mean you should. I'd would urge to to avoid setting scalar variables within a trigger at all costs. Even if you 100% sure your table will never have more that 1 row inserted per transaction because that's how the app is designed... You'll be in for very rude awakening when you find out that not all transactions come through the application.
Below is a quick demonstration of both types of triggers...
USE tempdb;
GO
IF OBJECT_ID('tempdb.dbo.PrimaryTable', 'U') IS NOT NULL
DROP TABLE dbo.PrimaryTable;
GO
IF OBJECT_ID('tempdb.dbo.TriggerScalarLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerScalarLog;
GO
IF OBJECT_ID('tempdb.dbo.TriggerMultiRowLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerMultiRowLog;
GO
CREATE TABLE dbo.PrimaryTable (
Pt_ID INT NOT NULL IDENTITY (1,1) PRIMARY KEY CLUSTERED,
Col_1 INT NULL,
Col_2 DATE NOT NULL
CONSTRAINT df_Col2 DEFAULT (GETDATE())
);
GO
CREATE TABLE dbo.TriggerScalarLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
CREATE TABLE dbo.TriggerMultiRowLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
--=======================================================
CREATE TRIGGER dbo.PrimaryCrudScalar ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
DECLARE
#Pt_ID INT,
#Col1_Old INT,
#Col1_New INT,
#Col2_Old DATE,
#Col2_New DATE;
SELECT
#Pt_ID = ISNULL(i.Pt_ID, d.Pt_ID),
#Col1_Old = d.Col_1,
#Col1_New = i.Col_1,
#Col2_Old = d.Col_2,
#Col2_New = i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
INSERT dbo.TriggerScalarLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
VALUES (#Pt_ID, #Col1_Old, #Col1_New, #Col2_Old, #Col2_New);
GO -- DROP TRIGGER dbo.PrimaryCrudScalar;
CREATE TRIGGER PrimaryCrudMultiRow ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
INSERT dbo.TriggerMultiRowLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
SELECT
ISNULL(i.Pt_ID, d.Pt_ID),
d.Col_1,
i.Col_1,
d.Col_2,
i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
GO -- DROP TRIGGER dbo.TriggerMultiRowLog;
--=======================================================
--=======================================================
-- --insert test...
INSERT dbo.PrimaryTable (Col_1)
SELECT TOP 100
o.object_id
FROM
sys.objects o;
SELECT 'INSERT Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'INSERT Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
UPDATE pt SET
pt.Col_1 = pt.Col_1 + rv.RandomVal,
pt.Col_2 = DATEADD(DAY, rv.RandomVal, pt.Col_2)
FROM
dbo.PrimaryTable pt
CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 10000 + 1) ) rv (RandomVal);
SELECT 'UPDATE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'UPDATE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
DELETE pt
FROM
dbo.PrimaryTable pt;
SELECT 'DELETE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'DELETE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
You could, but I'd recommend against it. If your source table changed things would start failing.
Also, in your example if you were to ever have more than one row inserted at a time you would get thrown an error (or have unpredictable results). I'd recommend a more set based approach:
INSERT table2 ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table2 t ON i.RecoID = t.RecoID
WHERE t.RecoID IS NULL;
EDIT:
If you want to stop the insert happening on your original table then you'll need to do something along the lines of:
CREATE TRIGGER trigger_name
ON table_orig
INSTEAD OF INSERT
AS
BEGIN
-- make sure we aren't triggering from ourselves from another trigger
IF TRIGGER_NESTLEVEL() <= 1
return;
-- insert into the table_copy if the inserted row is already in table_orig (not null)
INSERT table_copy ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NOT NULL;
-- insert into table_orig if the inserted row is not already in table_orig (null)
INSERT table_orig ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NULL;
END;
The instead of will stop the insert if you don't want it to actually be inserted, so you'll need to do that yourself (the second insert statement).
Please note I changed some nulls to not nulls and the table we are left joining to in some cases.

Resources