SQL: Set variable to a cell value inside a stored procedure - sql

I am trying to store a single cell value from one table in a variable (inside a stored procedure), so I can use it to edit a value in another table, but I keep getting a MSG 201:
Procedure or function 'spBookReturn' expects parameter '#bookID', which was not supplied.
Every time I try to run the sp where it all should happen:
CREATE PROC spBookReturn
#loanID UNIQUEIDENTIFIER,
#bookID UNIQUEIDENTIFIER OUTPUT
AS
BEGIN
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial SET returned = 1, returnedDate = GETDATE();
SET #bookID = (SELECT TOP 1 bookID FROM BorrowedMaterial WHERE loanID = #loanID ORBER BY returnedDate);
UPDATE Books SET nHome = nHome + 1 WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END;
EXEC spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F'
Does anyone know why the way I have defined the #bookID variable doesn't work and how I can make it work?
Thanks.
EDIT:
I got two tables: BorrowedMaterial that includes the attributes loanID, bookID, returned, returnedDate and a few others that's not relevant.
The other table is Books and it includes bookID, nHome but not the loanID.
So by giving only the loanID as an input, I would like to update the nHome. I am trying to grab bookID since it is the only thing the two attributes got in common and this is where the issues happen.
Side note: I removed the variable #custID it spawned by mistake.

All parameters for a procedure are Input parameters. If you declare a parameter as an OUTPUT parameter, it is still an input one, and if it doesn't have a default value must be supplied.
If you want the OUTPUT parameters to be option, which I personally find can be quite often, then give them a default value. I also add some additional logic to your procedure, as you should be using an TRY...CATCH and an ORDER BY in your query with a TOP.
CREATE PROC dbo.spBookReturn #loanID uniqueidentifier,
#bookID uniqueidentifier = NULL OUTPUT,
#custID uniqueidentifier = NULL OUTPUT
AS
BEGIN
BEGIN TRY --If you are using tranasctions, make sure you have a ROLLBACK and THROW for errors
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial
SET returned = 1,
returnedDate = GETDATE()
WHERE loanID = #loanID;
/*
UPDATE BorrowedMaterial
SET returnedDate = GETDATE()
WHERE loanID = #loanID;
*/
SET #bookID = (SELECT TOP 1 bookID
FROM BorrowedMaterial
WHERE loanID = #loanID
ORDER BY ???); --A TOP should have an ORDER BY
UPDATE Books
SET nHome = nHome + 1
WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION tBookReturn;
THROW;
END CATCH;
END;
Then you can execute the procedure as you have, without #bookID and #custID being passed. Of course, if you don't, their values will be "lost" in the calling statement. If you need them, then pass their values too in the EXEC:
DECLARE #bookID uniqueidentifier, #CustID uniqueidentifier;
EXEC dbo.spBookReturn #loanID, #bookID OUTPUT, #CustID OUTPUT;
--SELECT #bookID, #CustID;

declare #bookID UNIQUEIDENTIFIER;
declare #custID UNIQUEIDENTIFIER;
declare #InResult;
exec #InResult= spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F',#bookID,#custID;
select #bookID,#custID;
Seems, you need something like this

My take on what you're looking for. Because there are multiple DML statements (2 Updates) in a single transaction the XACT_ABORT ON option ensures a complete rollback. Also, the THROW in the CATCH block is placed before the ROLLBACK to preserve the SQL generated error metadata. Prior to executing the proc the OUTPUT variables are declared and placed in the parameter list (this omission is what was causing the initial error).
drop proc if exists dbo.spBookReturn;
go
CREATE PROC dbo.spBookReturn
#loanID uniqueidentifier,
#bookID uniqueidentifier OUTPUT,
#custID uniqueidentifier OUTPUT
AS
set nocount on;
set xact_abort on;
BEGIN TRANSACTION tBookReturn
BEGIN TRY
declare #ins_bm table(book_id uniqueidentifier,
cust_id uniqueidentifier);
UPDATE BorrowedMaterial
SET returned = 1,
returnedDate = GETDATE()
output inserted.book_id, inserted.cust_id into #ins_bm
WHERE loanID = #loanID;
if ##rowcount=0
begin
select #bookID=0,
#custID=0;
throw 50000, 'No update performed', 1;
end
else
begin
UPDATE b
SET nHome = nHome + 1
from Books b
WHERE exists (select 1
from #ins_bm bm
where b.ID = bm.book_id);
select top(1) #bookID=book_id,
#custID=cust_id
from #ins_bm;
end
COMMIT TRANSACTION tBookReturn;
END TRY
BEGIN CATCH
THROW
ROLLBACK TRANSACTION tBookReturn;
END CATCH;
go
declare #bookID UNIQUEIDENTIFIER;
declare #custID UNIQUEIDENTIFIER;
declare #InResult int;
exec #InResult=spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F', #bookID output, #custID output;
select #bookID, #custID;

Thanks to #Larnu I figured out the only thing missing was a = on line 3 (and thanks to #charlieface I also got my code cleaned up a tiny bit):
CREATE PROC spBookReturn
#loanID UNIQUEIDENTIFIER,
#bookID UNIQUEIDENTIFIER = NULL OUTPUT
AS
BEGIN
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial SET returned = 1, returnedDate = GETDATE();
SET #bookID = (SELECT TOP 1 bookID FROM BorrowedMaterial WHERE loanID = #loanID
ORDER BY returnedDate);
UPDATE Books SET nHome = nHome + 1 WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END;
EXEC spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F'

Related

How to use results from an SQL Query as a list to delete (WSUS) updates

My problem is that I want to use the results from a SELECT query as the input values for a Stored Procedure. The issue is that the SP will only accept Scalar values, and I do not know SQL and so have been struggling to find a workaround or solution.
I want to modify the following Proc to accept multiple values to be used within the query:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[spDeleteUpdateByUpdateID]
#updateID UNIQUEIDENTIFIER
AS
SET NOCOUNT ON
DECLARE #localUpdateID INT
SET #localUpdateID = NULL
SELECT #localUpdateID = LocalUpdateID FROM dbo.tbUpdate WHERE UpdateID = #updateID
IF #localUpdateID IS NULL
BEGIN
RAISERROR('The update could not be found.', 16, 40)
RETURN(1)
END
IF EXISTS (SELECT r.RevisionID FROM dbo.tbRevision r
WHERE r.LocalUpdateID = #localUpdateID
AND (EXISTS (SELECT * FROM dbo.tbBundleDependency WHERE BundledRevisionID = r.RevisionID)
OR EXISTS (SELECT * FROM dbo.tbPrerequisiteDependency WHERE PrerequisiteRevisionID = r.RevisionID)))
BEGIN
RAISERROR('The update cannot be deleted as it is still referenced by other update(s).', 16, 45)
RETURN(1)
END
DECLARE #retcode INT
EXEC #retcode = dbo.spDeleteUpdate #localUpdateID
IF ##ERROR <> 0 OR #retcode <> 0
BEGIN
RAISERROR('spDeleteUpdateByUpdateID got error from spDeleteUpdate', 16, -1)
RETURN(1)
END
RETURN (0)
TLDR: if anyone knows a quick way for me to use the results from SELECT UpdateID FROM tbUpdate WHERE UpdateTypeID = 'D2CB599A-FA9F-4AE9-B346-94AD54EE0629' to run exec spDeleteUpdateByUpdateID #updateID= i'd be extremely grateful.
There are some examples online of people using cursors to clean up WSUS. It will be slow but you are presumably only running it once. As mentioned there are other strategies for WSUS cleanup that should probably be investigated first.
DECLARE #var1 INT
DECLARE #msg nvarchar(100)
-- Get obsolete updates into temporary table
-- insert your own ID's here if you wish
CREATE TABLE #results (Col1 INT)
INSERT INTO #results(Col1) EXEC spGetObsoleteUpdatesToCleanup
DECLARE WC Cursor
FOR SELECT Col1 FROM #results
OPEN WC
FETCH NEXT FROM WC INTO #var1
WHILE (##FETCH_STATUS > -1)
BEGIN
SET #msg = 'Deleting' + CONVERT(varchar(10), #var1)
RAISERROR(#msg,0,1) WITH NOWAIT EXEC spDeleteUpdateByUpdateId #var1
FETCH NEXT FROM WC INTO #var1
END
CLOSE WC
DEALLOCATE WC
DROP TABLE #results

How to store value in stored procedure

I have a stored procedure that looks like this:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE Guests_Load
#Id AS Varchar(30)
AS
SET NOCOUNT ON
SELECT *
FROM Guests
WHERE HotelId = #Id
SELECT
GroupId,
HotelName,
FROM
HotelView(NOLOCK)
WHERE
HotelId = #Id
GO
Now, I want to create a new result set by writing another SELECT statement.
However, I want the groupId that is returned from the second SELECT. How do I do this? I tried:
DECLARE #hotelId int;
SELECT
#hotelId = GroupId,
HotelName,
FROM
HotelView(NOLOCK)
WHERE
HotelId = #Id
but I get an error saying
A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations
A simple solution is just to run the query twice:
CREATE PROCEDURE Guests_Load (
#Id AS Varchar(30)
) AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM Guests
WHERE HotelId = #Id;
SELECT GroupId, HotelName
FROM HotelView
WHERE HotelId = #Id;
DECLARE #GroupId int;
SELECT #GroupId = GroupId
FROM HotelView
WHERE HotelId = #Id;
END;
Personally, I am not a fan of stored procedures printing out tables. So, I would be inclined to have all the SELECTs use only variables. But if you want the print-out, then one method is to run the code twice.

Stored procedure unsure syntax

I am trying to create a stored procedure that will determine if my customerid exists if it exists then my other parameter foundcustomer will be assigned to found otherwise not found. I am unsure how to assign found please help
here is what i tried
CREATE PROCEDURE procedure4
-- Add the parameters for the stored procedure here
#FoundCustomer varchar(10) = null,
#Customerid varchar (5) = null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
if Not(#Customerid is null)
SELECT customerid
from customers
where customerid = #Customerid
END
GO
Gordon is right, it sounds like you may want a function but if it has to be a stored procedure you can follow this example.
CREATE PROCEDURE procedure4
#Customerid varchar(5) = null
AS
BEGIN
SET NOCOUNT ON;
DECLARE #FoundCustomer varchar(10) = ''
IF #FoundCustomer is not null
BEGIN
IF (SELECT COUNT(1) FROM customers WHERE customerid = #customerid) > 0
SET #FoundCustomer = 'Found'
ELSE
SET #FoundCustomer = 'Not Found'
END
SELECT #FoundCustomer
END

SQL Stored Procedure not handling error from nested stored procedure

I have two stored procedures, one nested inside the other. When the nested stored procedure is called, at the moment, it should error with a foreign key constraint violation and then rollback the earlier call to insert into the ProductLicense table. The nested procedure does not perform any action on the database because of the foreign key violation but the calling stored procedure isn't catching the error and rolling back. If I execute the nested stored procedure by itself it does return error 547 Foreign key violation.
How can I get the two stored procedures to work together?
Outer procedure:
ALTER PROCEDURE [dbo].[AddNewLicense2_i]
-- Add the parameters for the stored procedure here
#customerId nvarchar(10),
#licenseModeId int,
#licenseModeProgramId int,
#createdBy int,
#updateBy int,
#systemId nvarchar(50),
#productId int
AS
BEGIN TRY
BEGIN TRANSACTION
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--SET XACT_ABORT ON; --used for automatic rollback when an error occurs
DECLARE #tempDays INT
DECLARE #programCornerAmt INT
DECLARE #tempEndDate DATETIME
DECLARE #tempExpDate DATETIME
DECLARE #err INT
SET #err = 0
/*SET #tempDays = (SELECT lmp.TimeoutDays
FROM LicenseModeProgram lmp
WHERE lmp.LicenseModeProgramId = #licenseModeProgramId)*/
SELECT #tempDays = TimeoutDays, #programCornerAmt = MonthlyCornersAmount
FROM LicenseModeProgram
WHERE LicenseModeProgramId = #licenseModeProgramId
--Build Expiration and End Dates.
IF #tempDays = NULL --then this is NOT a time rental or metered system
BEGIN
SET #tempEndDate = NULL
SET #tempExpDate = NULL
END
ELSE
BEGIN
SET #tempEndDate = DATEADD("d", #tempDays, GETDATE())
SET #tempExpDate = DATEADD("d", #tempDays, GETDATE())
END
-- Create new product license record
INSERT INTO ProductLicense (CustomerId, LicenseModeId, LicenseModeProgramId, CreatedBy, UpdatedBy, SystemId, ProductId, ExpirationDate, LicenseEndDate)
VALUES (#customerId, #licenseModeId, #licenseModeProgramId, #createdBy, #updateBy, #systemId, #productId, #tempExpDate, #tempEndDate)
IF #licenseModeId = 4 AND #systemId NULL AND #programCornerAmt NULL
--call stored procedure to add corners to the customer account
EXECUTE #err = AddMeteredTx_i #systemId, 1, 1, #programCornerAmt , 'Initial License Creation'
PRINT #err
COMMIT TRANSACTION
END TRY
BEGIN CATCH
RAISERROR('Failed to Create License', 11, 2)
ROLLBACK TRANSACTION
RETURN 1
END CATCH
--COMMIT TRANSACTION
RETURN 0
GO
Inner procedure:
ALTER PROCEDURE [dbo].[AddMeteredTx_i]
-- Add the parameters for the stored procedure here
#systemId nvarchar(50),
#activityEventId int,
#createdBy int,
#amount int,
#notes text
AS
BEGIN TRY
BEGIN TRANSACTION
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--SET XACT_ABORT ON; --used for automatic rollback when an error occurs
INSERT INTO CustomerAccountActivity (SystemId, ActivityEventId, CreatedBy, Amount, Notes)
VALUES (#systemId, #activityEventId, #createdBy, #amount, #notes)
UPDATE CustomerAccount
SET MeteredBalance = (SELECT MeteredBalance FROM CustomerAccount WHERE SystemId = #systemId) + #amount
WHERE SystemId = #systemId
COMMIT TRANSACTION
END TRY
BEGIN CATCH
RAISERROR('Error Update to Customer Account Record ', 11, 2)
ROLLBACK TRANSACTION
RETURN 1
--COMMIT TRANSACTION
END CATCH
RETURN 0
GO
Catching errors with a call stack like this using ##Error can be problematic. It's a lot more reliable to use TRY/CATCH
The basic format is:
BEGIN TRY
<BEGIN TRAN>
... do stuff ...
<COMMIT TRAN>
END TRY
BEGIN CATCH
<ROLLBACK TRAN>
... do error stuff like re-raise the error to outer scope ...
END CATCH
Any error encountered in the try will automatically take you to the CATCH block without additional checking.

SQLserver multithreaded locking with TABLOCKX

I have a table "tbluser" with 2 fields:
userid = integer (autoincrement)
user = nvarchar(100)
I have a multithreaded/multi server application that uses this table.
I want to accomplish the following:
Guarantee that field user is unique in my table
Guarantee that combination userid/user is unique in each server's memory
I have the following stored procedure:
CREATE PROCEDURE uniqueuser #user nvarchar(100) AS
BEGIN
BEGIN TRAN
DECLARE #userID int
SET nocount ON
SET #userID = (SELECT #userID FROM tbluser WITH (TABLOCKX) WHERE [user] = #user)
IF #userID <> ''
BEGIN
SELECT userID = #userID
END
ELSE
BEGIN
INSERT INTO tbluser([user]) VALUES (#user)
SELECT userID = SCOPE_IDENTITY()
END
COMMIT TRAN
END
Basically the application calls the stored procedure and provides a username as parameter.
The stored procedure either gets the userid or insert the user if it is a new user.
Am I correct to assume that the table is locked (only one server can insert/query)?
I'm sure the following advice might help someone in the future.
Instead of (TABLOCKX), try (TABLOCKX, HOLDLOCK)
or even better, if this is the only procedure in which writing to tbluser takes place, you can cut down on a whole bunch of code entirely using only TABLOCKX and no transaction via
CREATE PROCEDURE uniqueuser #user nvarchar(100)
AS
--BEGIN TRAN NOT NECESSARY!
INSERT tbluser WITH (TABLOCKX) ([user])
SELECT #user
WHERE NOT EXISTS(SELECT 1 FROM tbluser WHERE [user]=#user)
--COMMIT TRAN NOT NECESSARY!
SELECT userID FROM tbluser WHERE [user]=#user
This way, the insert statement (which automatically creates a transaction for the duration of the INSERT) is the only time you need the table lock. (Yes, I stress-tested this on two windows both with and without TABLOCKX to see how it faired before posting my message.)
If you want to guarantee that the user is unique, the only way is to a unique constraint
ALTER TABLE tbluser WITH CHECK
ADD CONSTRAINT UQ_tbluser_user UQNIUE (user);
Do not "roll your own" unique checks: it will fail.
The cached data in the server's memory conforms to the same constraint
I'd do this. Look for user first, if not found insert, handle unique error just in case. And I'd use an OUTPUT parameter
CREATE PROCEDURE uniqueuser
#user nvarchar(100)
-- ,#userid int = NULL OUTPUT
AS
SET NOCOUNT, XACT_ABORT ON;
DECLARE #userID int;
BEGIN TRY
SELECT #userID FROM tbluser WHERE [user] = #user;
IF #userID IS NULL
BEGIN
INSERT INTO tbluser([user]) VALUES (#user);
SELECT userID = SCOPE_IDENTITY() ;
END
END TRY
BEGIN CATCH
-- test for a concurrent call that just inserted before we did
IF ERROR_NUMBER() = 2627
SELECT #userID FROM tbluser WHERE [user] = #user;
ELSE
-- do some error handling
END CATCH
-- I prefer output parameter for this SELECT #userID AS UserID
GO
Edit: why TABLOCKX fails...
You only lock the table for the duration of the SELECT.
A 2nd process running concurrently will starting reading the table after the lock is released by process 1
Both processes can have #userID IS NULL because process 1 has not yet executed the INSERT
Process 2 gets an error when it INSERTS
This happens because TABLOCKX modifies lock isolation and granularity, not duration.
Edit 2: for SQL Server 2000
CREATE PROCEDURE uniqueuser
#user nvarchar(100)
-- ,#userid int = NULL OUTPUT
AS
SET NOCOUNT, XACT_ABORT ON;
DECLARE #userID int;
SELECT #userID FROM tbluser WHERE [user] = #user;
IF #userID IS NULL
BEGIN
INSERT INTO tbluser([user]) VALUES (#user);
IF ##ERROR = 2627
SELECT #userID FROM tbluser WHERE [user] = #user;
ELSE
RAISERROR ('the fan needs cleaning', 16, 1);
SELECT userID = SCOPE_IDENTITY();
END
GO