Throw error on invalid insertion in SQL Server - sql-server

I have a SQL Server 2012 database with two tables:
CREATE TABLE Products
(
Id INT IDENTITY(1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product
PRIMARY KEY CLUSTERED (Id ASC)
);
CREATE TABLE BlockedProductCodes
(
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes
PRIMARY KEY CLUSTERED (Code ASC)
);
I want to be able to prevent products being inserted into the Products table if their product code exists in the BlockedProductCodes table.
The only way I could think of doing this was with a BEFORE INSERT trigger:
CREATE TRIGGER trg_Products_BEFORE_INSERT
ON Products
INSTEAD OF INSERT AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT Code
FROM BlockedProductCodes BPC
INNER JOIN inserted I ON BPC.Code = I.Code)
BEGIN
RAISERROR('The product has been blocked!', 16, 1);
END
ELSE
BEGIN
INSERT Product (Id, Code, Name)
SELECT Id, Code, Name
FROM INSERTED
END
SET NOCOUNT OFF;
END
But this caused an error with the identity column:
Cannot insert explicit value for identity column in table 'Products' when IDENTITY_INSERT is set to OFF
Can anyone suggest a way to fix this or a better approach?
Please note, this check is also made at the application level, but I want enforce that at the data table level.
Thanks.
Update: using check constraint
I have tried the following that seems to work..
CREATE FUNCTION dbo.IsCodeBlocked
(
#code nvarchar(50)
)
RETURNS BIT
WITH SCHEMABINDING
AS
BEGIN
DECLARE #ret bit
IF (#Code IN (SELECT Code FROM dbo.BlockedProductCodes))
SET #ret = 1
ELSE
SET #ret = 0
RETURN #ret
END
GO
ALTER TABLE Products
ADD CONSTRAINT CheckValidCode
CHECK (dbo.IsCodeBlocked(Code) = 0);
GO
insert Products (Code, Name) values ('xyz', 'Test #1')
go
insert Products (Code, Name) values ('abc', 'Test #2')
-- Fails with "The INSERT statement conflicted with the
-- CHECK constraint 'CheckValidCode'."
go
I am not sure if it is particularly 'safe' or performant. I will also test out the indexed view approach suggested by Damien.

One way you can implement this is by abusing an indexed view:
CREATE TABLE dbo.Products (
Id INT IDENTITY (1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product PRIMARY KEY CLUSTERED (Id ASC)
);
GO
CREATE TABLE dbo.BlockedProductCodes (
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes PRIMARY KEY CLUSTERED (Code ASC)
);
GO
CREATE TABLE dbo.Two (
N int not null,
constraint CK_Two_N CHECK (N > 0 and N < 3),
constraint PK_Two PRIMARY KEY (N)
)
GO
INSERT INTO dbo.Two(N) values (1),(2)
GO
create view dbo.DRI_NoBlockedCodes
with schemabinding
as
select
1 as Row
from
dbo.Products p
inner join
dbo.BlockedProductCodes bpc
on
p.Code = bpc.Code
inner join
dbo.Two t
on
1=1
GO
CREATE UNIQUE CLUSTERED INDEX IX_DRI_NoBlockedCodes on dbo.DRI_NoBlockedCodes (Row)
And now we attempt to insert:
INSERT INTO dbo.BlockedProductCodes (Code,ReasonCode) values ('abc',10)
GO
INSERT INTO dbo.Products (Code,Name) values ('abc','def')
And we get:
Msg 2601, Level 14, State 1, Line 42
Cannot insert duplicate key row in object 'dbo.DRI_NoBlockedCodes' with unique index 'IX_DRI_NoBlockedCodes'. The duplicate key value is (1).
The statement has been terminated.
So if that error message is acceptable to you, this could be one way to go. Note, that if you have a numbers table, you can use that instead of my dummy Two table.
The trick here is to construct the view in such a way so that, if there's ever a match between the Products and BlockedProductCodes tables, we produce a multi-row result set. But we've also ensured that all rows have a single constant column value and there's a unique index on the result - so the error is generated.
Note that I've used my convention of prefixing the table name with DRI_ when it exists solely to enforce an integrity constraint - I don't intend that anyone will ever query this view (indeed, as shown above, this view must always in fact be empty)

Related

Is Identity Column hold the duplicate values?

I have the table with the identity column. This is looks like identity column is duplicated. What are all the possibilities that column hold the duplicate values.
Table structure looks like below,
create table Table1(ID INT identity, value varchar(10))
if the column has no unique constraint, it is possible to get duplicate values in it.
One way to do this is like this
SET IDENTITY_INSERT Table1 ON
INSERT INTO Table1 (ID, value)
VALUES (1, 'hello')
SET IDENTITY_INSERT Table1 OFF
Another way is to reseed the table, check the answer of Satheesh on how to do that.
If you dont want that, make this column primary key or create an unique constraint on that column.
how to make a unique index :
CREATE UNIQUE NONCLUSTERED INDEX idx_Table1_ID
ON dbo.Table1(ID)
WHERE ID IS NOT NULL;
The WHERE ID IS NOT NULL; is needed in your example because you allow null values in the ID column, which I do not recommend
I am reasonably sure your table should look like this
Create table Table1 (
ID int identity not null,
Value varchar(10),
constraint PK_Table1_ID primary key (ID)
)
Of course and Identity Column can have Duplicate values.
But since the values are auto-populated by the Table itself, there is no chance that the Duplicates can be created by the system.
But you can add Duplicate values using the INSERT INTO Statement. or if you RESEED the identity column without removing the Existing values the system itself will create Duplicates.
because of The identity column in Not having a UNIQUE constraint by default.
see the below Example
CREATE TABLE Temp
(
SeqNo INT IDENTITY(1,1),
MyStr VARCHAR(10)
)
INSERT INTO Temp
VALUES('A')
Result
SeqNo MyStr
1 A
SET IDENTITY_INSERT TEMP ON
INSERT INTO TEMP(SeqNo,MyStr)
VALUES(1,'B')
SET IDENTITY_INSERT TEMP OFF
Result
SeqNo MyStr
1 A
1 B
Inserted Couple more records
INSERT INTO TEMP
VALUES('C')
INSERT INTO TEMP
VALUES('D')
Result
SeqNo MyStr
1 A
1 B
2 C
3 D
Perform identity Reseed and insert a New value
DBCC CHECKIDENT ('TEMP', RESEED, 1)
INSERT INTO TEMP
VALUES('E')
Final result
SeqNo MyStr
1 A
1 B
2 C
3 D
2 E

SQL Server: check constraint if relationship exists, else insert

I am trying to create a constraint to validate if a relation exists
I have tried to create a procedure and then use it in check constraint. Apparently that does not seem to work.
These are my tables:
STOCKITEMS table:
StockItemId INT
StockItemName VARCHAR
ColorId INT
COLOR table:
ColorId INT
ColorName VARCHAR
This is my stored procedure:
CREATE PROCEDURE USP_ValidateColor
(#Color NVARCHAR(50))
AS
IF NOT EXISTS(SELECT ColorName FROM WareHouse.Colors WHERE ColorName = #Color)
BEGIN
DECLARE #Id INT
SET #Id = (SELECT TOP(1) ColorId + 1 FROM Warehouse.Colors
ORDER BY ColorId DESC)
INSERT INTO Warehouse.Colors
VALUES (#Id, #Color)
PRINT 'Does not exist';
END;
ELSE
PRINT 'Exists';
So if a user insert into the table stock items, I want a check that checks if the colorId already exists in the color table
If it does not, then insert that colorname into colors and. I was thinking about using a constraint check with my procedure, but can't fix the query.
Don't use an SP to check a constraint, use a foreign key:
CREATE TABLE Colour (ColourID int PRIMARY KEY, --This should really have a name
ColourName varchar(20));
CREATE TABLE StockItem (StockItemID int PRIMARY KEY, --This should really have a name too
StockItemName varchar(20),
ColourID int);
ALTER TABLE dbo.StockItem ADD CONSTRAINT Colour_FK FOREIGN KEY (ColourID) REFERENCES dbo.Colour(ColourID);
Then, if you try to insert something into the StockItem table, it'll fail unless the colour exists:
INSERT INTO dbo.Colour (ColourID,
ColourName)
VALUES (1,'Green'),(2,'Blue');
GO
INSERT INTO dbo.StockItem (StockItemID,
StockItemName,
ColourID)
VALUES(1,'Paint',1); --works
GO
INSERT INTO dbo.StockItem (StockItemID,
StockItemName,
ColourID)
VALUES (1,'Wood Panels',3); --fails
GO
--clean up
DROP TABLE dbo.StockItem;
DROP TABLE dbo.Colour;
For checking, use a UNIQUE check constraint. If you want to insert a color only if it doesn't exist, use INSERT .. FROM .. WHERE to check for existence and insert in the same query.
The only "trick" is that FROM needs a table. This can be fixed using a table value constructor to create tables out of the values to insert. If the stored procedure accepts a table-valued parameter, there's no problem.
This example uses a LEFT JOIN to insert non-matching values :
declare #colors table (Color nvarchar(10) UNIQUE)
insert into #colors VALUES ('green')
select * from #colors;
insert into #Colors (Color)
select new.Color
from (VALUES ('red'),
('green')) new(Color)
left outer join #Colors old on old.Color=new.Color
where old.Color is NULL
-- (1 row affected)
insert into #Colors (Color)
select new.Color
from (VALUES ('red'),
('green')) new(Color)
left outer join #Colors old on old.Color=new.Color
where old.Color is NULL
-- (0 rows affected)
select * from #colors;
-- green
-- red
The same using a subquery:
insert into #Colors (Color)
select new.Color
from
(VALUES ('red'),
('green')) new(Color)
where not exists (select 1
from #colors
where color=new.Color);
By using the UNIQUE constraint we ensure that duplicate entries can't be inserted

Unique constraint based upon Grandparents in SQL Server

I have the following table structure:
Grandparent - GrandParentId (PK)
Parent - ParentId (PK), GrandParentId (FK)
Child - ChildId (PK), ChildTypeId (FK), ParentId (FK)
I want a unique constraint saying that two children cannot have the same ChildTypeId if they have a common GrandParentId. Is this possible with SQL Server?
You can't do it with a UNIQUE constraint, but you can do it with a CHECK constraint that calls a UDF.
Write a UDF that takes a ChildId and queries a JOIN of Child and Parent to see if there is another child with the same GrandParentID and ChildTypeId. If there is, return true/false.
Then in the CHECK constraint, call that function, passing the ChildId, and check if the result of the function is true/false.
You can also do it with a TRIGGER, but I prefer constraints.
to speed up others stumbling upon this post, here is an SQL example of what Tab Alleman is suggesting:
CREATE FUNCTION Func_CousinsWithSameChildTypeId
(
#childId uniqueidentifier,
#parentId uniqueidentifier,
#childTypeId uniqueidentifier
)
RETURNS bit
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar bit
SET #ResultVar = CASE
--assuming nullable - remove as necessary
WHEN #parentId IS NOT NULL AND #childTypeId IS NOT NULL AND EXISTS(
SELECT 1
FROM dbo.Parents AS pAll
INNER JOIN dbo.Children AS c ON c.ParentId = pAll.Id
WHERE pAll.GrandParentId IN
(SELECT p1.GrandparentId
FROM dbo.Parents AS p1
WHERE p1.Id = #ParentId)
AND c.Id <> #childId AND c.childTypeId = #childTypeId
)
THEN 1
ELSE 0
END
-- Return the result of the function
RETURN #ResultVar
END
GO
then add the check constraint with something like
ALTER TABLE [dbo].[Children] WITH CHECK ADD CONSTRAINT [CheckCousinsChildTypeId] CHECK (([dbo].[Func_CousinsWithSameChildTypeId]([Id],[ParentId],[ChildTypeId])=(0)))
GO
ALTER TABLE [dbo].[Children] CHECK CONSTRAINT [CheckCousinsChildTypeId]
GO

Dead Lock occur when foreign key exists on table

There are two table exist in DB, Audit and AuditField, following is the Create table code:
-- Primary key: ID
CREATE TABLE [dbo].[Audit](
[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[TypeName] [varchar](50) NOT NULL
)
GO
-- Primary key: ID
CREATE TABLE [dbo].[AuditField](
[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[AuditID] [int] NOT NULL,
[Field1] [varchar](50) NOT NULL
)
GO
-- Set foreign key on AuditField table
ALTER TABLE [dbo].[AuditField]
ADD CONSTRAINT [FK_AuditFiled_Audit] FOREIGN KEY([AuditID])
REFERENCES [dbo].[Audit] ([ID])
GO
Then I prepared some test data:
DECLARE #audit TABLE
(
ID int not null,
TypeName varchar(50)
)
DECLARE #auditField TABLE
(
AuditID int not null,
Field1 varchar(50)
)
-- ADD TEST DATA
DECLARE #i int = 1
DECLARE #rowCount int = 500
WHILE #i<=#rowCount
BEGIN
INSERT INTO #audit
VALUES(#i, 'SomeTypeName')
INSERT INTO #auditField
(AuditID,Field1)
VALUES(#i,'SomeThing')
SET #i += 1
END
Finally, i run following transaction to insert test data to these two table:
begin transaction
INSERT INTO dbo.Audit
SELECT TypeName
FROM #audit
ORDER BY ID
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
INSERT INTO dbo.AuditField
SELECT AuditID+#offSet AS AuditID, Field1
FROM #auditField
ORDER BY AuditID
commit transaction
When this transaction run concurrent, dead lock occur, one process are failed, the other got an error:
Msg 547, Level 16, State 0, Line 40 The INSERT statement conflicted
with the FOREIGN KEY constraint "FK_AuditFiled_Audit". The conflict
occurred in database "MyDB", table "dbo.Audit", column 'ID'.
There is no trigger on Audit and AuditField table.
Sorry for the format of the code, I really need an answer why this dead lock occur, thanks.
One thing should be clear, the data of AuditField table comes from #auditField, As #Bogdan answer I rewrite like this:
begin transaction
INSERT INTO dbo.Audit
OUTPUT inserted.ID INTO #temp
SELECT TypeName
FROM #audit
INSERT INTO #idMapping
SELECT ROW_NUMBER() OVER(ORDER BY ID) AS RowNumber, ID
FROM #temp
INSERT INTO dbo.AuditField
SELECT m.ID AS AuditID, Field1
FROM #auditField af
INNER JOIN #idMapping m ON af.AuditID = m.RowNumber
commit transaction
This is and Read - Write deadlock:
As you can see, every transaction has successfully acquired an [e]X[clusive] lock and it requests a S[hared] lock. The question is why a transaction try to read rows X locked by another transaction.
And the answer is bellow:
1) Following piece of source code
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
assumes that IDENTITY values generated by every
INSERT INTO dbo.Audit
SELECT TypeName
FROM ...
statement are continues. This is completely wrong as you can see in the following picture:
This means that at some point in time, a transaction could successfully get X locks on inserted rows and then
1) Because inserted rows into Audit aren't continuous and
2) Because of
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
INSERT INTO dbo.AuditField
SELECT AuditID+#offSet AS AuditID, Field1 ...
this last INSERT tries to insert into dbo.AuditField, AuditID values that belong to another transaction and this requires FK validation and, also, means that SQL Server needs to read rows from dbo.Audit. For this S[hared] locks are needed.
To be clear: the root cause of this deadlock is not the FK constraint. The real problem is that source code.
Solution: I would rewrite thus:
begin transaction
INSERT INTO dbo.Audit
OUTPUT inserted.ID, inserted.TypeName INTO #audit (ID, TypeName)
SELECT TypeName
FROM #audit
-- ORDER BY ID -- Isn't necessary
... do something (ex. DELETE) with rows from #audit
INSERT INTO dbo.AuditField (AuditID, ...)
SELECT x.ID, ...
FROM #audit x
-- ORDER BY AuditID
/* or
INSERT INTO dbo.AuditField (AuditID, Field1, ....)
SELECT y.ID, y.ColumnName, ...
FROM (
SELECT x.ID, ...
FROM #audit x
UNPIVOT( ColumnValue FOR ColumnName IN ([TypeName], ...) )
) y
WHERE y.....
*/
commit transaction -- Isn't necessary
You are trying to insert an invalid foreign key value into dbo.AuditField:
SELECT AuditID+#offSet AS AuditID, Field1
Why the #offset? You won't necessarily have an AuditId with that value in the dbo.Audit table.

Split table and insert with identity link

I have 3 tables similar to the sctructure below
CREATE TABLE [dbo].[EmpBasic](
[EmpID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[Name] [varchar](50),
[Address] [varchar](50)
)
CREATE TABLE [dbo].[EmpProject](
[EmpID] [int] NOT NULL primary key, // referencing column with EmpBasic
[EmpProject] [varchar](50) )
CREATE TABLE [dbo].[EmpFull_Temp](
[ObjectID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[T1Name] [varchar](50) ,
[T1Address] [varchar](50) ,
[T1EmpProject] [varchar](50)
)
The EmpFull_Temp table has the records with a dummy object ID column... I want to populate the first 2 tables with the records in this table... But with EmpID as a reference between the first 2 tables.
I tried this in a stored procedure...
Create Table #IDSS (EmpID bigint, objID bigint)
Insert into EmpBasic
output Inserted.EmpID, EmpFull_Temp.ObjectID
into #IDSS
Select T1Name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from #IDSS as A, EmpFull_Temp as B
Where A.ObjID = B.ObjectID
But it says.. The multi-part identifier "EmpFull_Temp.ObjectID" could not be bound.
Could you please help me in achieving this...
Edit : There is no guarantee that [Name]+[Address] would be unique across [EmpBasic] Table
With your EmpProject join table, you probably don't want the primary key constraint on only the EmpID column
DECLARE #Count int
DECLARE #NextEmpID int
DECLARE #StartObjectID int
DECLARE #EndObjectID int
-- range of IDs to transfer (inclusive)
SET #StartObjectID = 1
SET #EndObjectID = 105
BEGIN TRAN
-- lock tables so IDENT_CURRENT is valid
SELECT #Count = COUNT(*) FROM [EmpBasic] WITH (TABLOCKX, HOLDLOCK)
SELECT #Count = COUNT(*) FROM [EmpProject] WITH (TABLOCKX, HOLDLOCK)
SELECT #NextEmpID = IDENT_CURRENT('EmpBasic')
SET IDENTITY_INSERT [EmpBasic] ON
INSERT [EmpBasic] ([EmpID], [Name], [Address])
SELECT #NextEmpID + ROW_NUMBER() OVER(ORDER BY ObjectID), [T1Name], [T1Address]
FROM [EmpFull_Temp]
WHERE [ObjectID] BETWEEN #StartObjectID AND #EndObjectID
SET IDENTITY_INSERT [EmpBasic] OFF
INSERT [EmpProject]([EmpID], [EmpProject])
SELECT #NextEmpID + ROW_NUMBER() OVER(ORDER BY ObjectID), [T1EmpProject]
FROM [EmpFull_Temp]
WHERE [ObjectID] BETWEEN #StartObjectID AND #EndObjectID
COMMIT TRAN
The solution to this problem depends on whether the "parent" table (i.e. the one with the IDENTITY column) has a natural key (i.e. one or more fields which, when combined, are guaranteed to be unique, other than the surrogate primary key).
For example, in this case, is the combinaton of Name and Address aways going to be unique?
If the answer is yes then you can simply insert into EmpBasic without bothering to output and store the generated IDs. You can then insert into EmpProject joining back on to EmpBasic using the natural key (e.g. name and address) to fnd the correct EmpID.
Insert into EmpBasic
Select T1Name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from EmpBasic as A, EmpFull_Temp as B
Where A.Name = B.Name And A.Address = B.Address
If the answer is no then there is no easy solution I know of - in SQL Server 2005 (I've no idea if this is any different in 2008), it's not possible to OUTPUT values that are not inserted. I've got around this issue in the past by using one of the other fields (e.g. Name) to temporarily store the original ID (in this case, ObjectID), use that to join when inserting the child records as described above and then gone back to update the parent records o remove/replace the temporary values. It's not nice but I've not found a better way.
Insert into EmpBasic
Select cast(ObjectID as varchar(50)) as name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from EmpBasic as A, EmpFull_Temp as B
Where A.Name = cast(B.ObjectID as varchar(50))
Update EmpBasic
Set Name = B.T1Name
from EmpBasic as A, EmpFull_Temp as B
Where A.Name = cast(B.ObjectID as varchar(50))
Please note: I've not tested the sample SQL given above but I hope it gives you an idea of how you might approach this.
Add an ObjectID column to the EmpBasic table to facilitate the data transfer then drop it when you're done. I'm assuming this is a one-time operation, I don't recommend adding and dropping a column if this is on-going
I have used the Stack Exchange Data Explorer to investigate alternative solutions. The only one with promise at the moment is shown here. It is effectively #ScotHauder's answer, except using a temporary table that has the ObjectID column and using IDENTITY_INSERT to move the generated EmpId values into EmpBasic.
If you have to do this multiple times you need to get the EmpBasic_Temp EmpId IDENTITY starting value to be Max(EmpBasic.EmpID)+1.

Resources