AdventureWorks 2012 SQL Error when running a special procedure - sql-server

I am creating a special procedure that applies a inputted discount to an inputted applied quantity but I keep receiving an error where the PK can not be null. Why is it not auto incrementing the row?
GO
CREATE PROCEDURE Sales.uspExcesInvSale
#DiscountPct smallmoney,
#ProductInventory int
AS
SET IDENTITY_INSERT Sales.SpecialOffer ON
INSERT INTO Sales.SpecialOffer (SpecialOfferID, Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty)
VALUES ((SELECT MAX(SpecialOfferID)+1 FROM SpecialOffer), 'New Sale', #DiscountPct, 'Excess Inventory', 'Direct', DATEADD(DAY,5,GETDATE()), DATEADD(DAY,7,GETDATE()), #ProductInventory);
INSERT INTO Sales.SpecialOfferProduct (ProductID)
SELECT ProductID
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)
SET IDENTITY_INSERT Sales.SpecialOffer OFF;
GO
BEGIN TRY
EXEC Sales.uspExcesInvSale .5, 1800;
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber
,ERROR_SEVERITY() AS ErrorSeverity
,ERROR_STATE() AS ErrorState
,ERROR_PROCEDURE() AS ErrorProcedure
,ERROR_LINE() AS ErrorLine
,ERROR_MESSAGE() AS ErrorMessage;
END CATCH;
GO
Error received is
Error Number 515 Severity 16 Error State 2 Error Procedure Sales.uspExcesInvSale Error Line 9 Error Message Cannot insert the value NULL into column 'SpecialOfferID', table 'AdventureWorks2012.Sales.SpecialOfferProduct'; column does not allow nulls. INSERT fails.

The error is in here - you're not inserting a value for for SpecialOfferID, which is required for this table.
INSERT INTO Sales.SpecialOfferProduct (ProductID) -- These should have
SELECT ProductID -- SpecialOfferID as well
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)
I believe the PKs for the two tables are
SpecialOffer: single column called SpecialOfferID, int, IDENTITY (e.g., is an auto-increment)
SpecialOfferProduct: two int columns, of which only one is an IDENTITY. The other is SpecialOfferID
When inserting into SpecialOfferProduct, you therefore need to explicitly specify the SpecialOfferID as it's not automatically entered. The other part of the PK (SalesOrderDetailID I believe) is an IDENTITY and you can just let it do its work.
In contrast - given that the PK of SpecialOffer is an IDENTITY column, I suggest letting it do its work. Remove the IDENTITY_INSERT SET statements, and then don't include a value into the insert statement. It will automatically create the next ID value.
I'm guessing you want to get the relevant SpecialOfferID from the first insert (e.g., from SpecialOffer), and then use that in the second insert (e.g., into SpecialOfferProduct).
You can get the 'last inserted ID' using SCOPE_IDENTITY().
With those changes, here's an example set of code (sadly untested as I don't have the db set up).
NOTE - I am also not 100% sure what you're doing with these inserts (particularly the second) - you will need to check that it's doing what you want. This answer is related to the error re not able to do the insert.
DECLARE #NewID int
INSERT INTO Sales.SpecialOffer (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty)
VALUES ('New Sale', #DiscountPct, 'Excess Inventory', 'Direct', DATEADD(DAY,5,GETDATE()), DATEADD(DAY,7,GETDATE()), #ProductInventory);
SET #NewID = SCOPE_IDENTITY()
INSERT INTO Sales.SpecialOfferProduct (SpecialOfferID, ProductID)
SELECT #NewID, ProductID
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)

Related

What is the correct way of using ##IDENTITY

Question regarding the ##IDENTITY, I have 4 different tables:
Customer [Id]
Person [Id, fname, lname]
Account [Cd, owner, balance]
Transaction [Id, account, type]
Customer Id has a feature of identity increment 1.
My goal is to create a new person for the database, so that
Customer.Id = Person.Id = Account.owner = Transaction.ID
I have tried the following below, however I get this error:
Cannot insert null value into column owner
How do I correct the mistakes to make it work?
BEGIN TRAN
BEGIN TRY
INSERT INTO bank.customer DEFAULT VALUES
INSERT INTO bank.person (id, fname, lname)
VALUES (##IDENTITY, 'Mike', 'Phelps')
INSERT INTO bank.account (cd, owner, balance)
VALUES (2, ##IDENTITY, 0)
INSERT INTO bank.transaction (id, account, type)
VALUES (##IDENTITY, (SELECT cd FROM pankki.tili,'P')
END TRY
BEGIN CATCH
ROLLBACK
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_STATE() AS ErrorState,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH
I suspect what you want is this:
BEGIN TRY
BEGIN TRAN;
DECLARE #ID int; --bigint, decimal?
INSERT INTO bank.customer DEFAULT VALUES;
SET #ID = SCOPE_IDENTITY(); --Assumes customer has a column with an IDENTITY
INSERT INTO bank.person (id,fname,lname)
VALUES(#ID,'Mike','Phelps');
INSERT INTO bank.account (cd,owner,balance)
VALUES(2,#ID,0);
INSERT INTO bank.transaction(id,account,type)
SELECT #ID,
cd,
'P'
FROM pankki.tili; --I assume, therefore, that pankki.tili only ever has 1 row
COMMIT; --YOu were missing this
END TRY
BEGIN CATCH
ROLLBACK;
THROW; --Don't SELECT the error details, THROW it.
END CATCH
From the Microsoft document:
After an INSERT, SELECT INTO, or bulk copy statement is completed, ##IDENTITY contains the last identity value that is generated by the statement. If the statement did not affect any tables with identity columns, ##IDENTITY returns NULL.
I take it PERSON does not have an identity column, so when you insert into it, ##identity becomes NULL.
If you want to user the ##identity from the insert for the other tables, use it to set the value of a variable.
declare #PersistentID int;
INSERT INTO bank.customer DEFAULT VALUES
set #PersistentID = ##IDENTITY -- or scope_identity() is safer
INSERT INTO bank.person (id,fname,lname)
VALUES( #PersistentID ,'Mike','Phelps')
First, you need to understand the difference between this two commands:
##identity returns the last inserted identity value in ANY table in the current session, regardless of scope.
IDENT_CURRENT('table_or_view') returns the last inserted identity value for a GIVEN table.
So, for your case, you need to use the second one.
And your script would be something like this:
BEGIN TRAN
BEGIN TRY
INSERT INTO bank.customer DEFAULT VALUES
SET #customerID = (SELECT IDENT_CURRENT('bank.customer'))
INSERT INTO bank.person (id,fname,lname)
VALUES( #customerID,'Mike','Phelps')
INSERT INTO bank.account (cd,owner,balance)
VALUES(2,#customerID,0)
INSERT INTO bank.transaction(id,account,type)
VALUES(#customerID,(SELECT cd FROM pankki.tili,'P')
END TRY
This way you can guarantee that the same ID in inserted in the four tables.
If you are using ##identity this value is changing with every new insert.

Insert query as string in database on insert trigger

I am trying to create a trigger that will fire every time a new row is created in a table. When that happens, an insert query will be prepared that reflects how the row was inserted. This query will be stored to a table in another database.
To illustrate:
Database A has table products (id, name, duration, isavailable).
Database B has a table mysql_query (id, query)
When a new row (1001, 'test product', 365, 1) is inserted into A.products, the trigger should insert the following into B.mysql_query:
INSERT INTO products (id, name, duration, value, isavailable)
VALUES (1001, test product, 365, 1, 1)
Value by default will always be 1 for the moment.
The code for the trigger so far is:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER c_products
ON migrimi_test.dbo.products
AFTER INSERT
AS
DECLARE #c_id INT;
SET #c_id = (SELECT id from inserted);
DECLARE #c_name INT;
SET #c_name = (SELECT name from inserted);
DECLARE #c_duration INT;
SET #c_duration = (SELECT duration from inserted);
DECLARE #c_isavailable INT;
SET #c_isavailable = (SELECT isvisible from inserted);
BEGIN
SET NOCOUNT ON
INSERT INTO migrimi_temp.dbo.mysql_query (id, mysql_query)
VALUES (DEFAULT, 'INSERT INTO products (id, name, duration, value, isavailable) values ('+CAST(#c_id as nvarchar(50))+', '+'"'+#c_name'+'"'+, '+#c_duration+', 1, '+#c_isavailable+')' )
END
GO
I run the trigger and latter perform an insert. There is an error saying:
Conversion failed when converting the nvarchar value 'test product' to data type int.
I understand that it refers to the substitution of variable #c_name but this is the first trigger I ever write, and I can't tell what exactly is wrong.
A and B are SQL databases.
Ok, there were 3 problems with the trigger:
1- As WEI_DBA pointed out, #c_name was declared int, even though it was in fact an varchar.
2- Apparently every int variable should be cast as a varchar or it will throw the following error:
sql trigger Conversion failed when converting the varchar value to
datatype int
I still don't have this figured out 100% - this is the solution but I am googling the reason why. So don't take my word for granted and feel free to provide more reliable sources or alternative explanations / solutions. After all I am a trigger newbie.
3- While the trigger itself will work, the query inserted in the table mysql_query will be invalid unless the value #c_name (or any varchar value) is surrounded by double quotes.
The final result of a working trigger is the following:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER c_products
ON migrimi_test.dbo.products
AFTER INSERT
AS
DECLARE #c_id INT;
DECLARE #c_name nvarchar(100);
DECLARE #c_duration int;
DECLARE #c_isavailable INT;
Select #c_id = id, #c_name = name, #c_duration = duration, #c_isavailable = isvisible from inserted
BEGIN
SET NOCOUNT ON
INSERT INTO migrimi_temp.dbo.sql_query (query)
VALUES ('INSERT INTO products (id, name, duration, value, isavailable, createdAt, updatedAt) values ('+CAST(#c_id as nvarchar(50))+', '+'"'+#c_name+'"'+',
'+CAST(#c_duration as nvarchar(50))+', 1, '+CAST(#c_isavailable as nvarchar(50))+', Now(), Now());' )
END
GO
Ignore the new fields, there were modifications that I made but have nothing to do with the issue at hand.

Finding accounts greater than specified transaction amount

I have a bank db with two tables accountmaster and transactionmaster.
Accountmaster has columns:
accid(pk)
accname
bal
branch
Transactionmaster columns:
Tnumber(pk)
dot
txnAmt
transactiontype
accid(fk)
branch(fk)
I want to find the below without using instead of triggers.
whenever a transaction is made by the accountholder (transaction type like deposit, withdraw) it should reflect in the balance of accountmaster table.
whenever an accountholder makes a transaction > 50000 (withdraw or deposit), that transaction details are to be inserted into a new table 'hightransaction' and remove that particular transaction in the transactionmaster table.
I tried something like this but in the result only column names are displayed and no values.
First I copied the transactionmaster into newtable hightransaction
select *
into hightransaction
from transactionmaster
where 1 = 2
then I created a trigger
create trigger [dbo].[transaction2]
on transactionmaster
for insert
as
declare #transtype nvarchar(10);
select #transtype = [TXNTYPE]
from inserted
if (select txnamt from inserted) > 75000
begin
insert into [dbo].[hightransactionmaster3]
select
dot, txntype, chqnum, chqdate, txnamt,
acid, brid, userid
from
inserted
end
else
begin
insert into [dbo].[TRANSACTIONMASTER]
select
dot, txntype, chqnum, chqdate, txnamt,
acid, brid, userid
from
inserted
end
and I tried to execute
select * from hightransaction
The output is only column names and no values.
I am thinking of a stored procedure like this.
CREATE PROCEDURE [dbo].[transaction]
#transactionAmt int
/*
ADD OTHER PARAMETERS HERE
*/
AS
BEGIN
SET NOCOUNT ON;
IF (#transactionAmt > 50000)
BEGIN
/*INSERT STATEMENT FOR HIGHTRANSCTION TABLE*/
END
ELSE
BEGIN
/*INSERT STATEMENT FOR TRANSACTIONMASTER TABLE*/
END
END
GO
This is provided that you have control over the application and can have it pass the parameters into a stored procedure.

Trigger with a RAISERROR and ELSE case issue

I am trying to make a bit of code that takes in 2 separate columns, a month and a year. From there I want it to see if those numbers entered have already passed or not. If they have passed, cause an error to pass and stop the transaction. Otherwise, I want it to continue on and insert new information into the table. I know I am close on getting this to work, but I cant seem to get the RAISERROR to fire. I am sure it has to do with the fact I am pretty new at this and I am missing some small detail.
Currently I am taking the two months in as variables and the making a third variable to use to turn the other two into a proper datetime format. Then I use the datediff function to try and see if it has passed that way. To no avail though. I keep getting the insert function going, even if the card date is old.
USE AdventureWorks2012
GO
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF INSERT
AS
Begin
DECLARE #ExpMonth tinyint,
#ExpYear smallint,
#ExpMonthYear datetime
SELECT #ExpMonth=ExpMonth,
#ExpYear=ExpYear,
#ExpMonthYear = #ExpYear + '-' + #ExpMonth + '-00'
FROM INSERTED
IF
DATEDIFF(MONTH,#ExpMonthYear,GETDATE()) < 0
BEGIN
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ROLLBACK TRANSACTION
END
ELSE
Begin
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
Select CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate FROM inserted
END
End
I think there is a simpler way to check for expiration:
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS (
SELECT 1
FROM inserted
WHERE (YEAR(GETDATE()) > ExpYear) OR (YEAR(GETDATE()) = ExpYear AND MONTH(GETDATE()) > ExpMonth)
)
BEGIN
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ROLLBACK TRANSACTION
END
ELSE
BEGIN
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
SELECT CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate
FROM inserted
END
END
In this way you effectively check every record to be inserted in CreditCard.

Select / Insert version of an Upsert: is there a design pattern for high concurrency?

I want to do the SELECT / INSERT version of an UPSERT. Below is a template of the existing code:
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE)
The query will be called from many concurrent sessions. My performance tests show that it will consistently throw primary key violations under a specific load.
Is there a high-concurrency method for this query that will allow it to maintain performance while still avoiding the insertion of data that already exists?
You can use LOCKs to make things SERIALIZABLE but this reduces concurrency. Why not try the common condition first ("mostly insert or mostly select") followed by safe handling of "remedial" action? That is, the "JFDI" pattern...
Mostly INSERTs expected (ball park 70-80%+):
Just try to insert. If it fails, the row has already been created. No need to worry about concurrency because the TRY/CATCH deals with duplicates for you.
BEGIN TRY
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE -- only error was a dupe insert so must already have a row to select
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
Mostly SELECTs:
Similar, but try to get data first. No data = INSERT needed. Again, if 2 concurrent calls try to INSERT because they both found the row missing the TRY/CATCH handles.
BEGIN TRY
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
IF ##ROWCOUNT = 0
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
The 2nd one appear to repeat itself, but it's highly concurrent. Locks would achieve the same but at the expense of concurrency...
Edit:
Why not to use MERGE...
If you use the OUTPUT clause it will only return what is updated. So you need a dummy UPDATE to generate the INSERTED table for the OUTPUT clause. If you have to do dummy updates with many calls (as implied by OP) that is a lot of log writes just to be able to use MERGE.
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
-- be sure to have a non-clustered unique index on RowValue and RowID as your clustered index.
IF EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
ELSE BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
As always, gbn's answer is correct and ultimately lead me to where I needed to be. However, I found a particular edge case that wasn't covered by his approach. That being a 2601 error which identifies a Unique Index Violation.
To compensate for this, I've modified his code as follow
...
declare #errornumber int = ERROR_NUMBER()
if #errornumber <> 2627 and #errornumber <> 2601
...
Hopefully this helps someone!

Resources