Cannot insert the value NULL into column with procedure - sql

This is not a duplicate.
I do understand what the issue means but I don't understand why because the variable contains data. I'm basically trying to make a char(4) column increase alone (just like identity with integers). If the table doesn't contain anything, the first value would be 'C001' otherwise, It simply increase based on the last record.
CREATE PROCEDURE ADD_CL(#nom VARCHAR(20),
#dn DATE)
AS
BEGIN
DECLARE #B CHAR(4)
DECLARE #B_to_int INT
DECLARE #B_new_value CHAR(4)
IF EXISTS(SELECT TOP 1 *
FROM CLIENT)
SET #B_new_value = 'C001'
ELSE
BEGIN
SELECT TOP 1 #B = code_client
FROM client
ORDER BY code_client DESC
SET #B_to_int = CAST(SUBSTRING(#B, 2, 3) AS INTEGER)
SET #B_to_int = #B_to_int + 1;
SET #B_new_value = LEFT(#B, 1) + RIGHT('00' + CAST(#B_to_int AS INT), 3)
END
INSERT INTO CLIENT
VALUES (#B_new_value,
#nom,
#dn)
END
Cannot insert the value NULL into column 'code_client', table 'dbo.CLIENT'; column does not allow nulls. INSERT fails.
#B_new_value represent code_client

Your If Exists should be If Not Exists.
So change
if exists(select TOP 1 * from CLIENT)
to
if not exists(select TOP 1 * from CLIENT)
Also you are adding 00 to your final #B_to_int which is cast as int. so it will show C2,C3 and so on.
If you want to retain the same format, cast it to varchar
SET #B_new_value = LEFT(#B,1) + '00' + CAST(#B_to_int as varchar)
Above line will work only till the count is 9. and then it will continue replicating itself with 1 because 10 will be 0010 and final output will be C0010. To eliminate this issue, use replicate and replicate 0 until 3 characters.
SET #B_new_value = LEFT(#B,1) + REPLICATE('0',3-LEN(#B_to_int)) + #B_to_int
Good Luck.

The other answers already tell you that you should be using NOT EXISTS.
This numbering scheme is quite possibly something you'll regret but you could simplify this a lot as well as making it safer in conditions of concurrency and when you run out of numbers by just doing
CREATE PROCEDURE ADD_CL(#nom VARCHAR(20),
#dn DATE)
AS
BEGIN
DECLARE #B VARCHAR(5);
SET XACT_ABORT ON;
BEGIN TRAN
SELECT #B = FORMAT(1 + RIGHT(ISNULL(MAX(code_client), 'C000'), 3), '\C000')
FROM CLIENT WITH(ROWLOCK, UPDLOCK, HOLDLOCK);
IF ( LEN(#B) > 4 )
THROW 50000, 'Exceeded range',1;
INSERT INTO CLIENT
VALUES (#B,
#nom,
#dn);
COMMIT
END

I believe the following should be 'NOT EXISTS'
if EXISTS(select TOP 1 * from CLIENT)

Related

How to set total number of rows before an OFFSET occurs in stored procedure

I've created a stored procedure that filters and paginates for a DataTable.
Problem: I need to set an OUTPUT variable for #TotalRecords found before an OFFSET occurs, otherwise it sets #TotalRecord to #RecordPerPage.
I've messed around with CTE's and also simply trying this:
SELECT *, #TotalRecord = COUNT(1)
FROM dbo
But that doesn't work either.
Here is my stored procedure, with most of the stuff pulled out:
ALTER PROCEDURE [dbo].[SearchErrorReports]
#FundNumber varchar(50) = null,
#ProfitSelected bit = 0,
#SortColumnName varchar(30) = null,
#SortDirection varchar(10) = null,
#StartIndex int = 0,
#RecordPerPage int = null,
#TotalRecord INT = 0 OUTPUT --NEED TO SET THIS BEFORE OFFSET!
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM
(SELECT *
FROM dbo.View
WHERE (#ProfitSelected = 1 AND Profit = 1)) AS ERP
WHERE
((#FundNumber IS NULL OR #FundNumber = '')
OR (ERP.FundNumber LIKE '%' + #FundNumber + '%'))
ORDER BY
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'asc'
THEN ERP.FundNumber
END ASC,
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'desc'
THEN ERP.FundNumber
END DESC
OFFSET #StartIndex ROWS
FETCH NEXT #RecordPerPage ROWS ONLY
Thank you in advance!
You could try something like this:
create a CTE that gets the data you want to return
include a COUNT(*) OVER() in there to get the total count of rows
return just a subset (based on your OFFSET .. FETCH NEXT) from the CTE
So your code would look something along those lines:
-- CTE definition - call it whatever you like
WITH BaseData AS
(
SELECT
-- select all the relevant columns you need
p.ProductID,
p.ProductName,
-- using COUNT(*) OVER() returns the total count over all rows
TotalCount = COUNT(*) OVER()
FROM
dbo.Products p
)
-- now select from the CTE - using OFFSET/FETCH NEXT, get only those rows you
-- want - but the "TotalCount" column still contains the total count - before
-- the OFFSET/FETCH
SELECT *
FROM BaseData
ORDER BY ProductID
OFFSET 20 ROWS FETCH NEXT 15 ROWS ONLY
As a habit, I prefer non-null entries before possible null. I did not reference those in my response below, and limited a working example to just the two inputs you are most concerned with.
I believe there could be some more clean ways to apply your local variables to filter the query results without having to perform an offset. You could return to a temp table or a permanent usage table that cleans itself up and use IDs that aren't returned as a way to set pages. Smoother, with less fuss.
However, I understand that isn't always feasible, and I become frustrated myself with those attempting to solve your use case for you without attempting to answer the question. Quite often there are multiple ways to tackle any issue. Your job is to decide which one is best in your scenario. Our job is to help you figure out the script.
With that said, here's a potential solution using dynamic SQL.
I'm a huge believer in dynamic SQL, and use it extensively for user based table control and ease of ETL mapping control.
use TestCatalog;
set nocount on;
--Builds a temp table, just for test purposes
drop table if exists ##TestOffset;
create table ##TestOffset
(
Id int identity(1,1)
, RandomNumber decimal (10,7)
);
--Inserts 1000 random numbers between 0 and 100
while (select count(*) from ##TestOffset) < 1000
begin
insert into ##TestOffset
(RandomNumber)
values
(RAND()*100)
end;
set nocount off;
go
create procedure dbo.TestOffsetProc
#StartIndex int = null --I'll reference this like a page number below
, #RecordsPerPage int = null
as
begin
declare #MaxRows int = 30; --your front end will probably manage this, but don't trust it. I personally would store this on a table against each display so it can also be returned dynamically with less manual intrusion to this procedure.
declare #FirstRow int;
--Quick entry to ensure your record count returned doesn't excede max allowed.
if #RecordsPerPage is null or #RecordsPerPage > #MaxRows
begin
set #RecordsPerPage = #MaxRows
end;
--Same here, making sure not to return NULL to your dynamic statement. If null is returned from any variable, the entire statement will become null.
if #StartIndex is null
begin
set #StartIndex = 0
end;
set #FirstRow = #StartIndex * #RecordsPerPage
declare #Sql nvarchar(2000) = 'select
tos.*
from ##TestOffset as tos
order by tos.RandomNumber desc
offset ' + convert(nvarchar,#FirstRow) + ' rows
fetch next ' + convert(nvarchar,#RecordsPerPage) + ' rows only'
exec (#Sql);
end
go
exec dbo.TestOffsetProc;
drop table ##TestOffset;
drop procedure dbo.TestOffsetProc;

Identity Column Format

I'm converting some data to SQL, and one column is a ID column in the format of last 2 digits of this year, and increment +1 of the last entered row in database.
So this year first entry would be 15-001, the next entry would be 15-002 etc. I cant seem to find an example anywhere online on how to do this? i'm not sure if I could do this in a Computed Column? Using Sql Server 2012.
Thank you!
I highly do not recommend using an ID in the format that you are stating. You will come across many other issues. You can use this code in a stored procedure or function to get next ID value.
DECLARE
#oldID varchar(8),
#newID varchar(8),
#temp varchar(8),
#number decimal,
#day int;
begin
SET #oldID = '15-555'; --select statement for last id
SET #day = YEAR(CURRENT_TIMESTAMP);
IF ( LEN(#oldID) = 6)
SET #temp = SUBSTRING(#oldID, 4, 6);
ELSE
SET #temp = SUBSTRING(#oldID, 3, 5);
SET #number = CAST(#temp AS Decimal(10, 0));
SET #number += 1;
SET #temp = CAST(#number AS varchar(8));
WHILE LEN(#temp) < 3
begin
SET #temp = '0' + #temp;
end
SET #newID = SUBSTRING(CAST(#day AS varchar(8)), 3, 4) + '-' + #temp;
print #newID --ID that you are looking for.
end
Stops working correctly after 15-999 and does not reset to 16-001 if that is what your were asking. Code can be changed to account for these things.

How to get query result with stored procedure (convert item quantity from one table into my unit defined in second table)

I have two MSSQL2008 tables like this:
I have problem on the unit conversion logic.
The result I expect like this :
1589 cigar = 1ball, 5slop, 8box, 2pcs
52 pen = 2box, 12pcs
Basically I'm trying to take number (qty) from one table and to convert (split) him into the units which I defined in other table!
Note : Both table are allowed to add new row and new data (dinamic)
How can I get these results through a SQL stored procedure?
i totally misunderstand the question lest time so previous answer is removed (you can see it in edit but it's not relevant for this question)... However i come up with solution that may solve your problem...
NOTE: one little think about this solution, if you enter the value in second table like this
+--------+-------+
| Item | qty |
+--------+-------+
| 'cigar'| 596 |
+--------+-------+
result for this column will be
598cigar = 0ball, 5slop, 8box, 0pcs
note that there is a ball and pcs is there even if their value is 0, that probably can be fix if you don't want to show that value but I let you to play with it...
So let's back to solution and code. Solution have two stored procedures first one is the main and that one is the one you execute. I call it sp_MainProcedureConvertMe. Here is a code for that procedure:
CREATE PROCEDURE sp_MainProcedureConvertMe
AS
DECLARE #srcTable TABLE(srcId INT IDENTITY(1, 1), srcItem VARCHAR(50), srcQty INT)
DECLARE #xTable TABLE(xId INT IDENTITY(1, 1), xVal1 VARCHAR(1000), xVal2 VARCHAR(1000))
DECLARE #maxId INT
DECLARE #start INT = 1
DECLARE #sItem VARCHAR(50)
DECLARE #sQty INT
DECLARE #val1 VARCHAR(1000)
DECLARE #val2 VARCHAR(1000)
INSERT INTO #srcTable (srcItem, srcQty)
SELECT item, qty
FROM t2
SELECT #maxId = (SELECT MAX(srcId) FROM #srcTable)
WHILE #start <= #maxId
BEGIN
SELECT #sItem = (SELECT srcItem FROM #srcTable WHERE srcId = #start)
SELECT #sQty = (SELECT srcQty FROM #srcTable WHERE srcId = #start)
SELECT #val1 = (CAST(#sQty AS VARCHAR) + #sItem)
EXECUTE sp_ConvertMeIntoUnit #sItem, #sQty, #val2 OUTPUT
INSERT INTO #xTable (xVal1, xVal2)
VALUES (#val1, #val2)
SELECT #start = (#start + 1)
CONTINUE
END
SELECT xVal1 + ' = ' + xVal2 FROM #xTable
GO
This stored procedure have two variables as table #srcTable is basically your second table but instead of using id of your table it's create new srcId which goes from 1 to some number and it's auto_increment it's done because of while loop to avoid any problems when there is some deleted values etc. so we wanna be sure that there wont be any skipped number or something like that.
There is few more variables some of them is used to make while loop work other one is to store data. I think it's not hard to figure out from code what are they used for...
While loop iterate throughout all rows from #srcTable take values processing them and insert them into #xTable which basically hold result.
In while loop we execute second stored procedure which have a task to calculate how many unit of something is there in specific number of item. I call her sp_ConvertMeIntoUnit and here is a code for her:
CREATE PROCEDURE sp_ConvertMeIntoUnit
#inItemName VARCHAR(50),
#inQty INT,
#myResult VARCHAR(5000) OUT
AS
DECLARE #rTable TABLE(rId INT IDENTITY(1, 1), rUnit VARCHAR(50), rQty INT)
DECLARE #yTable TABLE(yId INT IDENTITY(1, 1), yVal INT, yRest INT)
DECLARE #maxId INT
DECLARE #start INT = 1
DECLARE #quentity INT = #inQty
DECLARE #divider INT
DECLARE #quant INT
DECLARE #rest INT
DECLARE #result VARCHAR(5000)
INSERT INTO #rTable(rUnit, rQty)
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
SELECT #maxId = (SELECT MAX(rId) FROM #rTable)
WHILE #start <= #maxId
BEGIN
SELECT #divider = (SELECT rQty FROM #rTable WHERE rId = #start)
SELECT #quant = (#quentity / #divider)
SELECT #rest = (#quentity % #divider)
INSERT INTO #yTable(yVal, yRest)
VALUES (#quant, #rest)
SELECT #quentity = #rest
SELECT #start = (#start + 1)
CONTINUE
END
SELECT #result = COALESCE(#result + ', ', '') + CAST(y.yVal AS VARCHAR) + r.rUnit FROM #rTable AS r INNER JOIN #yTable AS y ON r.rId = y.yId
SELECT #myResult = #result
GO
This procedure contain three parametars it's take two parameters from the first one and one is returned as result (OUTPUT). In parameters are Item and Quantity.
There are also two variables as table #rTable we stored values as #rId which is auto increment and always will go from 1 to some number no matter what is there Id's in the first table. Other two values are inserted there from the first table based on #inItemName parameter which is sanded from first procedure... From the your first table we use unit and quantity and stored them with rId into table #rTable ordered by Qty from biggest number to lowest. This is a part of code for that
INSERT INTO #rTable(rUnit, rQty)
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
Then we go into while loop where we do some maths. Basically we store into variable #divider values from #rTable. In the first iteration we take the biggest value calculate how many times it's contain into the number (second parameter we pass from first procedure is qty from the yours second table) and store it into #quant than we also calculate modulo and store it into variable #rest. This line
SELECT #rest = (#quentity % #divider)
After that we insert our values into #yTable. Before we and with iteration in while loop we assign #quentity variable value of #rest value because we need to work just with the remainder not with whole quantity any more. In second iteration we take next (the second greatest number in our #rTable) number and procedure repeat itself...
When while loop finish we create a string. This line here:
SELECT #result = COALESCE(#result + ', ', '') + CAST(y.yVal AS VARCHAR) + r.rUnit FROM #rTable AS r INNER JOIN #yTable AS y ON r.rId = y.yId
This is the line you want to change if you want to exclude result with 0 (i talk about them at the beginning of answer)...
And at the end we store result into output variable #myResult...
Result of this stored procedure will return string like this:
+--------------------------+
| 1ball, 5slop, 8box, 2pcs |
+--------------------------+
Hope I didn't miss anything important. Basically only think you should change here is the name of the table and their columns (if they are different) in first stored procedure instead t2 here
INSERT INTO...
SELECT item, qty
FROM t2
And in second one instead of t1 (and column if needed) here..
INSERT INTO...
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
Hope i help a little or give you an idea how this can be solved...
GL!
You seem to want string aggregation – something that does not have a simple instruction in Transact-SQL and is usually implemented using a correlated FOR XML subquery.
You have not provided names for your tables. For the purpose of the following example, the first table is called ItemDetails and the second one, Items:
SELECT
i.item,
i.qty,
details = (
SELECT
', ' + CAST(d.qty AS varchar(10)) + d.unit
FROM
dbo.ItemDetails AS d
WHERE
d.item = i.item
FOR XML
PATH (''), TYPE
).value('substring(./text()[1], 3)', 'nvarchar(max)')
FROM
dbo.Items AS i
;
For the input provided in the question, the above query would return the following output:
item qty details
----- ----------- ------------------------------
cigar 1598 1pcs, 1000ball, 12box, 100slop
pen 52 1pcs, 20box
You can further arrange the data into strings as per your requirement. I would recommend you do it in the calling application and use SQL only as your data source. However, if you must, you can do the concatenation in SQL as well.
Note that the above query assumes that the same unit does not appear more than once per item in ItemDetails. If it does and you want to aggregate qty values per unit before producing the detail line, you will need to change the query a little:
SELECT
i.item,
i.qty,
details = (
SELECT
', ' + CAST(SUM(d.qty) AS varchar(10)) + d.unit
FROM
dbo.ItemDetails AS d
WHERE
d.item = i.item
GROUP BY
d.unit
FOR XML
PATH (''), TYPE
).value('substring(./text()[1], 3)', 'nvarchar(max)')
FROM
dbo.Items AS i
;

How to use declared table values to delete from a table?

I'm passing a delimited string to a stored procedure that enters the values into the declared table when it runs into the delimiter,
Here is my Stored Procedure.
Alter PROCEDURE s_BulkDeleteTest
(
#IDString VarChar(200)
)
AS
-- Creating Variables
DECLARE #numberLength int
DECLARE #numberCount int
DECLARE #TheIDs VarChar(200)
DECLARE #sTemp VarChar(100) -- to hold single characters
-- Creating a temp table
DECLARE #T TABLE
(
TheIDs VarChar(500)
)
--Initializing Variables for counting
SET #numberLength = LEN (#IDString)
SET #numberCount = 1
SET #TheIDs = ''
--Start looping through the keyword ids
WHILE (#numberCount <= #numberLength)
BEGIN
SET #sTemp = SUBSTRING (#IDString, #numberCount, 1)
IF (#sTemp = ',')
BEGIN
INSERT #T(TheIDs) VALUES (#TheIDs)
SET #TheIDs = ''
END
IF (#sTemp <> ',')
BEGIN
SET #TheIDs = #TheIDs + #sTemp
END
SET #numberCount = #numberCount + 1
END
This all works fine for adding the values to the #T table, but then I added this..
delete from [Subjects]
where (select TheIDs from #T) = SubjectID
that threw an error about there being more than one value in the declared table #T.
So I was wondering how can I use the values in #T and delete all those ID's from my Subjects table.
If TheIDs has any null values using IN operator will delete unexpect rows. I would suggest using EXISTS operator something like this...
DELETE FROM [Subjects]
WHERE EXISTS
(SELECT 1
FROM #T
WHERE [Subjects].SubjectId = TheIDs)
You need to use in:
delete from [Subjects]
where SubjectId in (select TheIDs from #T);
A result set with multiple rows cannot be equal to a single value.
EDIT:
The expression (select TheIds from #T) returns a set of values. The = operator works on scalar values, not sets. So, it doesn't normally work with this construct. The in operator compares a scalar to a set. so it does work.
There is one exception. When the subquery returns one row and one column, then it is converted to a scalar value. So, the expression would work if there were one row returned, or if you forced one row, as in:
where SubjectId = (select top 1 TheIDs from #T);
Of course, in would work in this situation as well.

Generating/updating unique random barcode numbers in a column in SQL

We're a manufacturing company, and I have a SQL Table [FSDBGL] that holds information for every item we have. This includes columns for ItemNumber, ItemUPC, and ItemStatus. Some of the data in the ItemUPC column is empty for required items.
What I need to do, is assign/insert a random unique barcode number (that isn't already taken) inside of the ItemUPC column. The number needs to be 12 digits long, and be preceded with "601040xxxxxx", randomizing only the last 6 digits. This does not have to be done on every row for every item number.
-- Check/update only the [ItemNumber]'s (between 40000-01 - 50000-01) (the -01 at the end could also be a -02)
I need to ignore/exclude the following column attributes from getting a number:
-- ItemStatus (only if it's set at 'O' for Obsolete)
-- ItemUPC (if it already has a barcode number)
I would like to customize a SQL query for this that I can populate the cells now, and implement into a nightly process to update any newly-created Item#'s as well.
Here's the CREATE Script view:
USE [FSDBGL]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Mfg_ITMMAST](
[IMPN] [varchar](30) NOT NULL,
[IMDESC] [varchar](70) NOT NULL,
[IMUPCCD] [varchar](13) NOT NULL,
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
Random numbers: You could loop through each row that's missing the UPC using a cursor and get a random number for each one using ROUND(RAND() * 999999,0, 0), and then check for collisions before doing the update. The where clause for the cursor query should be pretty straightforward... regular expression on the ItemNumber, ItemStatus != 'O', ItemUPC != null or '' or 0 (or whatever the default value is).
The sproc should be rerunnable at any time, since it uses random numbers and checks for collisions.
A more efficient way would be to use serial-issued numbers instead of random. As long as you were able to store the last used number in a table somewhere, I believe you could add all the UPC numbers with one query instead of having to run several for each one by utilizing the UPDATE ... FROM syntax and the SELECT #counter = #counter + 1 syntax for user variables.
EDIT: Adding stored procedure and other comments
Let me first note that this database design is probably not optimal. There is no primary key on this table and no indexes. If this table has any large amount of records, queries are going to be slow, and this stored procedure is going to be very slow.
I also had to make some assumptions. Since the IMUPCCD can't be null, then I assume there is a default value of 601040 when the UPC is "blank". Since there was no primary key, I couldn't update through the cursor, but instead had to run a separate update statement, which is also slower. I also had to assume that IMPN uniquely identifies a row of data. I'm not sure if these assumptions are correct, so you may have to modify the sproc to suit your situation.
Also, the original question refers to ItemStatus, but no status column was given in the schema, so I couldn't limit results by it in my tests. However, you can easily add it to the stored procedure's DECLARE blanksCursor CURSOR FOR ... WHERE ... statement in the WHERE clause.
The Test Data (in a database called stackoverflow)
USE [stackoverflow]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
--DROP TABLE [dbo].[Mfg_ITMMAST];
CREATE TABLE [dbo].[Mfg_ITMMAST](
[IMPN] [varchar](30) NOT NULL,
[IMDESC] [varchar](70) NOT NULL,
[IMUPCCD] [varchar](13) NOT NULL
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'40000-01', N'test', N'601040')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'41023-01', N'test', N'601040123456')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'41001-02', N'test', N'601040')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'51001-01', N'test', N'601040')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'51001-02', N'test', N'601040')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'51014-02', N'test', N'601040234567')
INSERT [dbo].[Mfg_ITMMAST] ([IMPN], [IMDESC], [IMUPCCD]) VALUES (N'61001-01', N'test', N'601040')
The Stored Procedure
CREATE PROCEDURE uspScanForBlankUpcs
AS
-- setup variables for bringing in blank row data
DECLARE #IMPN [varchar](30), #IMUPCCD [varchar](13),
#blankUpc [varchar](13), #upcPrefix [varchar](6),
#random [varchar](6), #retryRandom bit;
SET #blankUpc = '601040'; -- This is the value of IMUPCCD when it is "blank"
SET #upcPrefix = '601040'; -- This is prefix for our randomly generated UPC
-- setup the cursor, query for items with "blank" UPCs
DECLARE blanksCursor CURSOR FOR
SELECT IMPN
FROM [Mfg_ITMMAST]
WHERE (LEFT(IMPN, 5) >= '40000' AND
LEFT(IMPN, 5) < '60000' AND
RIGHT(IMPN, 2) IN ('01','02')) AND
IMUPCCD = #blankUpc
;
-- open the cursor
OPEN blanksCursor;
-- load the next row from the cursor
FETCH NEXT FROM blanksCursor
INTO #IMPN;
-- loop through each row of the cursor
WHILE ##FETCH_STATUS = 0
BEGIN
--PRINT 'IMPN: ' + #IMPN;
-- try to create a new random number
SET #retryRandom = 1;
WHILE #retryRandom = 1
BEGIN
-- get a random number for the UPC, then left-pad it with zeros to 6 digits
SET #random = RIGHT('00000' + CONVERT(VARCHAR, FLOOR(RAND() * 999999)), 6);
-- concatenate the UPC prefix with the random number
SET #IMUPCCD = #upcPrefix + #random
--PRINT 'IMUPCCD: ' + #IMUPCCD;
-- see if this UPC already exists on another item
IF (SELECT COUNT(*) FROM [Mfg_ITMMAST] WHERE [IMUPCCD] = #IMUPCCD) > 0
SET #retryRandom = 1; -- UPC already existed (collision) try again
ELSE
SET #retryRandom = 0; -- didn't already exist, so exit out of loop
END
--PRINT 'Updating...';
-- Update the UPC with the random number
UPDATE [Mfg_ITMMAST]
SET IMUPCCD = #IMUPCCD
WHERE IMPN = #IMPN
;
-- Load the next result
FETCH NEXT FROM blanksCursor
INTO #IMPN;
END
CLOSE blanksCursor;
DEALLOCATE blanksCursor;
GO
Running the Stored Procedure
exec uspScanForBlankUpcs;
Resources that I used for this procedure:
MSDN - Creating Stored Procedure
MSDN - DECLARE CURSOR (Transact-SQL)
This answer focuses on how to assign UPC codes from your company prefix to new products when there are "gaps" in the sequence or you are reusing UPC codes from inactive product (not recommended).
UPC codes are made up of 3 parts:
The first is the company prefix.
The second is the item reference.
The third is the check digit.
In this example the company prefix is 0601040. The leading zero is beyond the scope of this question.
The item reference is a range of numbers the size of which depends up on the number of digits in the company prefix. In this example we have 6 digits in the company prefix (don't count the leading zero) and 1 digit for the check digit which leaves 5 digits for the item reference. This makes the item reference range 0 through 99999. You need to find numbers in this range that are not used.
The check digit is computed using the other 11 digits of the UPC code:
http://www.gs1.org/how-calculate-check-digit-manually
Unless you need to store invalid UPC codes because you accept bad data from an outside system there are advantages to storing the company prefix and item reference as separate fields in the table and making the UPC an indexed persistent computed field:
-- Function to output UPC code based on Company Prefix and Item Reference:
CREATE FUNCTION [dbo].[calc_UPC]
(#company_prefix varchar(10), #item_reference int)
RETURNS char(12)
WITH SCHEMABINDING
AS
BEGIN
declare
#upc char(12),
#checkdigit int
if SUBSTRING(#company_prefix, 1, 1) = 0
begin
set #upc = substring(#company_prefix, 2, 10) + right('000000000000' + ltrim(str(#item_reference)), 12 - len(#company_prefix))
set #checkdigit = (1000 - (
convert(int, substring(#upc, 1, 1)) * 3 +
convert(int, substring(#upc, 2, 1)) * 1 +
convert(int, substring(#upc, 3, 1)) * 3 +
convert(int, substring(#upc, 4, 1)) * 1 +
convert(int, substring(#upc, 5, 1)) * 3 +
convert(int, substring(#upc, 6, 1)) * 1 +
convert(int, substring(#upc, 7, 1)) * 3 +
convert(int, substring(#upc, 8, 1)) * 1 +
convert(int, substring(#upc, 9, 1)) * 3 +
convert(int, substring(#upc, 10, 1)) * 1 +
convert(int, substring(#upc, 11, 1)) * 3)) % 10
set #upc = rtrim(#upc) + ltrim(str(#checkdigit))
end
return #upc
END
GO
-- Example Table of products:
CREATE TABLE [dbo].[Product](
[product_id] [int] IDENTITY(1,1) NOT NULL,
[company_prefix] [varchar](10) NULL,
[item_reference] [int] NULL,
[upc] AS ([dbo].[calc_UPC]([company_prefix],[item_reference])) PERSISTED,
CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED
(
[product_id] ASC
))
ALTER TABLE [dbo].[Product] WITH CHECK ADD CONSTRAINT [item_reference_greater_equal_zero] CHECK (([item_reference]>=(0)))
GO
ALTER TABLE [dbo].[Product] CHECK CONSTRAINT [item_reference_greater_equal_zero]
GO
-- Existing records with UPC codes:
insert product (company_prefix, item_reference) values ( '0601040', 3)
insert product (company_prefix, item_reference) values ( '0601040', 5)
-- Example of 4 new products without UPC codes
insert product DEFAULT VALUES
insert product DEFAULT VALUES
insert product DEFAULT VALUES
insert product DEFAULT VALUES
GO
-- Next we need a table of all possible item references.
-- This is the best implementation I have found for generating numbers:
--Creates a table of sequential numbers, useful for all sorts of things
--Created 08/26/05 by Oskar Austegard from article at
--http://msdn.microsoft.com/library/en-us/dnsqlpro03/html/sp03k1.asp
--Limits: #Min and #Max must be between -2147483647 and 2147483647, including.
--If #Max <= #Min, only a single record with #Min is created
CREATE FUNCTION [dbo].[NumberTable] (#Min int, #Max int)
RETURNS #T TABLE (Number int NOT NULL PRIMARY KEY)
AS
BEGIN
-- Seed the table with the min value
INSERT #T VALUES (#Min)
--Loop until all the rows are created, inserting ever more records for each iteration (1, 2, 4, etc)
WHILE ##ROWCOUNT > 0
BEGIN
INSERT #T
--Get the next values by adding the current max - start value + 1 to each existing number
--need to calculate increment value first to avoid arithmetic overflow near limits of int
SELECT t.Number + (x.MaxNumber - #Min + 1)
FROM #T t
CROSS JOIN (SELECT MaxNumber = MAX(Number) FROM #T) x --Current max
WHERE
--Do not exceed the Max - shift the increment to the right side to take advantage of index
t.Number <= #Max - (x.MaxNumber - #Min + 1)
END
RETURN
END
GO
-- For 10,000 numbers the performance of this function is good,
-- but when the range is known I prefer the performance I get with a static table:
-- Create a table of numbers between 0 and 99999
CREATE table Numbers (number int)
insert Numbers (number)
select n.Number
from dbo.NumberTable(0, 99999) n
-- Now we can easily assign UPC codes using the available item reference values in your Company Prefix in a single update:
declare #company_prefix varchar(10)
set #company_prefix = '0601040' -- The function requires the leading zero
update
p
set
item_reference = n.number,
company_prefix = #company_prefix
from
(
select
p.product_id,
ROW_NUMBER() OVER (order by product_id) [row]
from
dbo.product p
where
p.company_prefix is null
) u
inner join dbo.product p on p.product_id = u.product_id
inner join
(
select
s.Number,
ROW_NUMBER() over (order by s.Number) [row]
from
(
select n.Number from
(
select
n.Number
from
dbo.Numbers n --Table(#sequence, #size - 1) n
left outer join dbo.Product p
on p.company_prefix = #company_prefix
and n.Number = p.item_reference
where
p.product_id is null
) n
) s
) n on n.[row] = u.[row]
GO
select * from product
Using this approach you don't need to worry about invalid check digits and you can easily assign UPC codes to new products from your GS1 assigned UCC block. It also makes it easy to start assigning UPC codes from a new company prefix. You can support EAN13 codes the same way with a small change to the function:
CREATE FUNCTION [dbo].[calc_EAN13]
(#company_prefix varchar(10), #item_reference int)
RETURNS char(13)
WITH SCHEMABINDING
AS
BEGIN
declare
#ean13 char(13),
#checkdigit int
set #ean13 = #company_prefix
set #ean13 = #company_prefix + right('0000000000000' + ltrim(str(#item_reference)), 12 - len(#company_prefix))
set #checkdigit = (1000 - (
convert(int, substring(#ean13, 1, 1)) * 1 +
convert(int, substring(#ean13, 2, 1)) * 3 +
convert(int, substring(#ean13, 3, 1)) * 1 +
convert(int, substring(#ean13, 4, 1)) * 3 +
convert(int, substring(#ean13, 5, 1)) * 1 +
convert(int, substring(#ean13, 6, 1)) * 3 +
convert(int, substring(#ean13, 7, 1)) * 1 +
convert(int, substring(#ean13, 8, 1)) * 3 +
convert(int, substring(#ean13, 9, 1)) * 1 +
convert(int, substring(#ean13, 10, 1)) * 3 +
convert(int, substring(#ean13, 11, 1)) * 1 +
convert(int, substring(#ean13, 12, 1)) * 3)) % 10
set #ean13 = rtrim(#ean13) + ltrim(str(#checkdigit))
return #ean13
END
GO