SQL Server: how to generate serial number by dynamic SQL - sql-server

create procedure test
(#TABLE_NAME varchar(20))
as
declare #lastval varchar(10)
set #lastval = right('000000000' + convert(varchar(10),
(select IsNull(max(Serialno), 0) + 1
from #TABLE_NAME)), 10)
return #lastval
end
Now tell me how I could compose or form dynamic SQL with above SQL where I will pass table name to store procedure when call that stored procedure?
How to return #lastval value to its calling environment?
How to call stored procedure test from another stored procedure where I will store the return value ?
Guide me with sample code.

Genearlly, it's best to use an IDENTITY or a SEQUENCE to assign serial numbers. A zero-padded string or other formatting requirements could be added to the table as a computed column based on the underlying serial integer or formatted in the app code. However, both IDENTITY and SEQUENCE may have gaps, such as due to rollbacks or SQL Server service restart.
In cases where an unbroken sequence of serial values is required by business, one can maintan the last assigned values in a table and assign values transactionally. Below is an example that returns the value using an OUTPUT parameter. Although the proc in your question uses the stored proc RETURN value for this purpose, that should only be used to indicate success or failure, not return data.
CREATE TABLE dbo.TableSerialNumber(
TableName sysname NOT NULL
CONSTRAINT PK_SerialNumber PRIMARY KEY
, SerialNumber int NOT NULL
, FormatString nvarchar(20) NULL
);
GO
INSERT INTO dbo.TableSerialNumber VALUES('Invoice', 0, '0000000000');
INSERT INTO dbo.TableSerialNumber VALUES('PurchaseOrder', 0, '0000000000');
GO
CREATE PROC dbo.GetNextSerialNumberForTable
#TableName sysname
, #FormattedSerialNumber varchar(10) OUTPUT
AS
SET NOCOUNT ON;
DECLARE
#SerialNumber int
, #FormatString nvarchar(20);
UPDATE dbo.TableSerialNumber
SET
#SerialNumber = SerialNumber += 1
, #FormatString = FormatString
WHERE TableName = #TableName;
IF ##ROWCOUNT = 0
RAISERROR('Table %s does not exist in dbo.TableSerialNumber', 16, 1, #TableName);
SET #FormattedSerialNumber = CAST(FORMAT(#SerialNumber, #FormatString) AS varchar(10));
GO
--example usage
CREATE PROC dbo.InsertInvoice
#InvoiceData int
AS
SET XACT_ABORT ON;
DECLARE #InvoiceNumber varchar(10);
BEGIN TRY
BEGIN TRAN;
EXECUTE dbo.GetNextSerialNumberForTable
#TableName = N'Invoice'
, #FormattedSerialNumber = #InvoiceNumber OUTPUT;
INSERT INTO dbo.Invoice (InvoiceID, InvoiceData)
VALUES(#InvoiceNumber, #InvoiceData);
SELECT #InvoiceNumber AS InvoiceNumber;
COMMIT;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
GO

Related

How to get and use the value returned by a stored procedure to a INSERT INTO... SELECT... statement

I am just new in SQL language and still studying it. I'm having hard time looking for answer on how can I use the stored procedure and insert value into a table.
I have this stored procedure:
CREATE PROCEDURE TestID
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NewID VARCHAR(30),
#GenID INT,
#BrgyCode VARCHAR(5) = '23548'
SET #GenID = (SELECT TOP (1) NextID
FROM dbo.RandomIDs
WHERE IsUsed = 0
ORDER BY RowNumber)
SET #NewID = #BrgyCode + '-' + CAST(#GenID AS VARCHAR (30))
UPDATE dbo.RandomIDs
SET dbo.RandomIDs.IsUsed = 1
WHERE dbo.RandomIDs.NextID = #GenID
SELECT #NewID
END;
and what I'm trying to do is this:
INSERT INTO dbo.Residents([ResidentID], NewResidentID, [ResLogdate],
...
SELECT
[ResidentID],
EXEC TestID ,
[ResLogdate],
....
FROM
source.dbo.Resident;
There is a table dbo.RandomIDs containing random 6 digit non repeating numbers where I'm pulling out the value via the stored procedure and updating the IsUsed column of the table to 1. I'm transferring data from one database to another database and doing some processing on the data while transferring. Part of the processing is generating a new ID with the required format.
But I can't get it to work Sad I've been searching the net for hours now but I'm not getting the information that I need and that the reason for my writing. I hope someone could help me with this.
Thanks,
Darren
your question is little bit confusing, because you have not explained what you want to do. As i got your question, you want to fetch random id from randomids table and after performed some processing on nextid you want to insert it into resident table [newresidentid] and end of the procedure you fetch data from resident table. if i get anything wrong feel free to ask me.
your procedure solution is following.
CREATE PROCEDURE [TestId]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NEWID NVARCHAR(30)
DECLARE #GENID BIGINT
DECLARE #BRGYCODE VARCHAR(5) = '23548'
DECLARE #COUNT INTEGER
DECLARE #ERR NVARCHAR(20) = 'NO IDS IN RANDOM ID'
SET #COUNT = (SELECT COUNT(NEXTID) FROM RandomIds WHERE [IsUsed] = 0)
SET #GENID = (SELECT TOP(1) [NEXTID] FROM RandomIds WHERE [IsUsed] = 0 ORDER BY [ID] ASC)
--SELECT #GENID AS ID
IF #COUNT = 0
BEGIN
SELECT #ERR AS ERROR
END
ELSE
BEGIN
SET #NEWID = #BRGYCODE + '-' + CAST(#GENID AS varchar(30))
UPDATE RandomIds SET [IsUsed] = 1 WHERE [NextId] = #GENID
INSERT INTO Residents ([NewResidentId] , [ResLogDate] ) VALUES (#NEWID , GETDATE())
SELECT * FROM Residents
END
END
this procedure will fetch data from your randomids table and perform some processing on nextid than after it directs insert it into resident table and if you want to insert some data through user you can use parameter after declaring procedure name
E.G
CREATE PROCEDURE [TESTID]
#PARAM1 DATATYPE,
#PARAM2 DATATYPE
AS
BEGIN
END
I'm not convinced that your requirement is a good one but here is a way to do it.
Bear in mind that concurrent sessions will not be able to read your update until it is committed so you have to kind of "lock" the update so you will get a block until you're going to commit or rollback. This is rubbish for concurrency, but that's a side effect of this requirement.
declare #cap table ( capturedValue int);
declare #GENID int;
update top (1) RandomIds set IsUsed=1
output inserted.NextID into #cap
where IsUsed=0;
set #GENID =(select max( capturedValue) from #cap )
A better way would be to use an IDENTITY or SEQUENCE to solve your problem. This would leave gaps but help concurrency.

Duplicate Auto Numbers generated in SQL Server

Be gentle, I'm a SQL newbie. I have a table named autonumber_settings like this:
Prefix | AutoNumber
SO | 112320
CA | 3542
A whenever a new sales line is created, a stored procedure is called that reads the current autonumber value from the 'SO' row, then increments the number, updates that same row, and return the number back from the stored procedure. The stored procedure is below:
ALTER PROCEDURE [dbo].[GetAutoNumber]
(
#type nvarchar(50) ,
#out nvarchar(50) = '' OUTPUT
)
as
set nocount on
declare #currentvalue nvarchar(50)
declare #prefix nvarchar(10)
if exists (select * from autonumber_settings where lower(autonumber_type) = lower(#type))
begin
select #prefix = isnull(autonumber_prefix,''),#currentvalue=autonumber_currentvalue
from autonumber_settings
where lower(autonumber_type) = lower(#type)
set #currentvalue = #currentvalue + 1
update dbo.autonumber_settings set autonumber_currentvalue = #currentvalue where lower(autonumber_type) = lower(#type)
set #out = cast(#prefix as nvarchar(10)) + cast(#currentvalue as nvarchar(50))
select #out as value
end
else
select '' as value
Now, there is another procedure that accesses the same table that duplicates orders, copying both the header and the lines. On occasion, the duplication results in duplicate line numbers. Here is a piece of that procedure:
BEGIN TRAN
IF exists
(
SELECT *
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
)
BEGIN
SELECT
#prefix = ISNULL(autonumber_prefix,'')
,#current_value=CAST (autonumber_currentvalue AS INTEGER)
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
SET #new_auto_number = #current_value + #number_of_lines
UPDATE dbo.autonumber_settings
SET autonumber_currentvalue = #new_auto_number
WHERE autonumber_type = 'SalesOrderDetail'
END
COMMIT TRAN
Any ideas on why the two procedures don't seem to play well together, occasionally giving the same line numbers created from scratch as lines created by duplication.
This is a race condition or your autonumber assignment. Two executions have the potential to read out the same value before a new one is written back to the database.
The best way to fix this is to use an identity column and let SQL server handle the autonumber assignments.
Barring that you could use sp_getapplock to serialize your access to autonumber_settings.
You could use repeatable read on the selects. That will lock the row and block the other procedure's select until you update the value and commit.
Insert WITH (REPEATABLEREAD,ROWLOCK) after the from clause for each select.

What could be causing the primary key exception?

My ASP pages store session variables in SQL Server with the following stored procedure:
CREATE PROCEDURE [dbo].[MyProcedure]
#sessionId varchar(512),
#variable varchar(350),
#value image
AS
BEGIN
BEGIN TRAN
DECLARE #result int = 0;
DECLARE #locked bit;
IF (SELECT COUNT(*) FROM Sessions WHERE id = #sessionId) = 0
BEGIN
SET #result = -1;
END
ELSE BEGIN
DELETE Variables WHERE sessionId = #sessionId AND variable = #variable
IF #value IS NOT NULL
BEGIN
INSERT Variables VALUES(#sessionId, #variable, #value, 0)
END
END
COMMIT TRAN
RETURN #result
END
But once in a while, I get a primary key exception (Msg 2627): "Violation of PRIMARY KEY constraint 'PK_Variables'. Cannot insert duplicate key in object 'dbo.Variables'".
Note: There are no triggers involved.
Thanks!
Assuming your PK is on sessionId,variable then concurrent executions of the stored procedure with the same #sessionId,#variable could do this.
Both execute the
DELETE Variables WHERE sessionId = #sessionId AND variable = #variable
line concurrently and then both proceed to the insert.
This could only occur if there is no pre-existing record with the sessionId,variable combination as then the DELETEs would block.

can we return a null from stored procedure

Can we return null value from stored procedure. i dont want to use collase or isnull. I want to capture NULL at the frontend. Is it possible ?
Edit:
I am using Sql Server 2005
eg. where i want to use
CREATE PROCEDURE [Authentication].[spOnlineTest_CheckLogin]
#UserName NVARCHAR(50)
AS
BEGIN TRY
BEGIN TRAN
COMMIT TRAN
RETURN NULL
END TRY
Error
The 'spOnlineTest_CheckLogin' procedure attempted to return a status of NULL, which is not allowed. A status of 0 will be returned instead.
Msg 0, Level 11, State 0, Line 0
A severe error occurred on the current command. The results, if any, should be discarded.
No, the return type of a stored procedure is INT and it cannot be null.
use an output parameter, example
CREATE PROCEDURE Test
#UserName NVARCHAR(50), #Status int output
AS
BEGIN TRY
BEGIN TRAN
COMMIT TRAN
set #Status = null
END TRY
begin catch
end catch
go
then call it like this
declare #s int
set #s =5
exec Test'bla',#s output
select #s --will be null
You can think of a proc like follows. Let me first set the context. We might have a table Table1(id int, name varchar(2), Address varchar(2)) and want to get the id and if not found, it will be null. So we might write a proc like the following:
CREATE PROCEDURE GetId
#Name VARCHAR(50), #Status int output
AS
BEGIN TRY
set #Status = null
select #Status = id from Table1 where name=#name
This will work for you.

result set based on success or failure

I'm having a stored procedure which returns two result sets based on the success or failure.
SP success result set: name, id ,error,desc
SP failure result sret: error,desc
I'm using the following query to get the result of the stored procedure. It returns 0 for success and -1 for failure.
declare #ret int
DECLARE #tmp TABLE (
name char(70),
id int,
error char(2),
desc varchar(30)
)
insert into #tmp
EXEC #ret = sptest '100','King'
select #ret
select * from #tmp
If the SP is success the four field gets inserted into the temp table since the column matches.
But in case of failure the sp result set has only error and desc which does not matchs with no of columns in the temp table...
.I can't change the Sp, so I need to do some thing (not sure) in temp table to handle both failure and success.
You can't return 2 different recordsets and load the same temp table.
Neither can try and fill 2 different tables.
There are 2 options.
Modify your stored proc
All 4 columns are returned in all conditions
1st pair (name, ID) columns are NULL on error
2nd pair (error, desc) are NULL on success
If you are using SQL Server 2005 then use the TRY/CATCH to separate your success and fail code paths. The code below relies on using the new error handling to pass back the error result set via exception/RAISERROR.
Example:
CREATE PROC sptest
AS
DECLARE #errmsg varchar(2000)
BEGIN TRY
do stuff
SELECT col1, col2, col3, col4 FROM table etc
--do more stuff
END TRY
BEGIN CATCH
SELECT #errmsg = ERROR_MESSAGE()
RAISERROR ('Oops! %s', 16, 1, #errmsg)
END CATCH
GO
DECLARE #tmp TABLE ( name CHAR(70), id INT, error char(2), desc varchar(30)
BEGIN TRY
insert into #tmp
EXEC sptest '100','King'
select * from #tmp
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE()
END CATCH
My fault!!
Was too quick in the answer.
You need only to relv on the return value, so building up the logic against it is much better.
If you still want to use the temp table, then calling the sptest twice could be a way to deal with it (not optimal though), one time to get the return value and based on it then have 2 different temp tables you are filling up (one would be with the 4 fields, the other only with 2 fields).
declare #ret int
DECLARE #tmp TABLE (name CHAR(70), id INT, error char(2), desc varchar(30))
DECLARE #tmperror TABLE (error char(2), desc varchar(30))
EXEC #ret = sptest '100','King'
IF #ret != 0
BEGIN
INSERT INTO #tmperror
EXEC sptest '100','King';
SELECT * FROM #tmperror;
END
ELSE
BEGIN
INSERT INTO #tmp
EXEC sptest '100','King';
SELECT * FROM #tmp;
END
Keep in mind that this solution is not optimal.
Try modifying your table definition so that the first two columns are nullable:
DECLARE #tmp TABLE (
name char(70) null,
id int null,
error char(2),
desc varchar(30)
)
Hope this helps,
Bill
You cannot do this with just one call. You will have to call it once, either getting the return status and then branching depending on the status to the INSERT..EXEC command that will work for the number of columns that will be returned or Call it once, assuming success, with TRY..CATCH, and then in the Catch call it again assuming that it will fail (which is how it got to the CATCH).
Even better, would be to either re-write the stored procedure so that it returns a consistent column set or to write you own stored procedure, table-valued function or query, by extracting the code from this stored procedure and adapting it to your use. This is the proper answer in SQL.

Resources