No idea how to handle an error while calling substring on a sql variable

I'm trying to figure out how to handle an error that gets thrown at the final set statement, wherein I call substring on the current @return value and set the result the same return value.

The stored procedure takes in a string of numbers like this: '135, 34, 21,'. The trailing comma is an artifact of a poorly designed system. This procedure runs the string through a split function to create a temp table, and then does a look up on each of the items in the string, returning a name associated with each of the three values. The error is thrown when all three values of the parameter are 0: '0,0,0,'

I'm just crap with SQL Server and have no idea how to gracefully handle those situations. Could someone provide some advice for me?

Here is my stored procedure:

declare @positions table
    (orderID int identity(1,1), positionID int)
declare @counter int
declare @positionMax int
declare @currentPositionID int
declare @return varchar(max)

set @counter = 1
set @return = ''

insert @positions
select items
from dbo.Split(
    (select Positions
     from dbo.athletes
     where athleteID = 3701, ',')

select @positionMax = max(orderID) 
from @positions

while(@counter <= @positionMax)
begin
    select @currentPositionID = tp.PositionID
    from db.positions tp
        inner join @positions p
            on tp.PositionID = p.positionID
    where p.orderID = @counter

    select @return = @return + PositionName + ', '
    from dbo.positions
    where PositionID = @currentPositionID

    set @counter = @counter + 1
end

set @return = substring(@return, 1, (len(@return) - 1))

select @return

Edit with sample data and expected results

Here's what the positions table looks like:

PositionId, PositionName
1, Defensive End
2, Quarterback
3, Pitcher
4, Catcher
5, First Base

The positions are saved in a single table row like this:

1,2,3,
1,0,0,
0,0,0,

The output would look like this:

Defensive End, Quarterback, Pitcher
Defensive End
No position selected

"No position selected" is what I'd like to have output in the instance of three zeros.


Solution 1:

I'm guessing your error is caused because @return is 0 characters long. A simple option could be to add the value 0, 'No position selected' to the positions table.

A nicer option could be to add an IF statement around the set statement:

if len(@return) > 0
BEGIN
    set @return = substring(@return, 1, (len(@return) - 1))
END
ELSE
BEGIN
    set @return = 'No position selected'
END

You could also add a Try/Catch block if using SQL to catch any generic exceptions. See msdn try/catch

Also, not sure why you're doing the while loop. There are generally better options in SQL like using a CTE.

Solution 2:

Here's one way to do it without the split function and without the loop. Note that this doesn't turn out in the exact order of your expected output.

DECLARE @pos TABLE
(
    PositionId   INT,
    PositionName VARCHAR(32)
);

INSERT @pos SELECT 1, 'Defensive End'
UNION ALL   SELECT 2, 'Quarterback'
UNION ALL   SELECT 3, 'Pitcher'
UNION ALL   SELECT 4, 'Catcher'
UNION ALL   SELECT 5, 'First Base';

DECLARE @row TABLE(pList VARCHAR(32));

INSERT @row SELECT '1,2,3,'
UNION ALL   SELECT '1,0,0,'
UNION ALL   SELECT '0,0,0,';

;WITH cte AS
(
    SELECT x.pList, p.PositionName 
    FROM @row AS x LEFT OUTER JOIN @pos AS p 
    ON ',' + x.pList LIKE '%,' + CONVERT(VARCHAR(12), p.PositionID) + ',%'
)
SELECT COALESCE(NULLIF(STUFF((
    SELECT ', ' + PositionName 
      FROM cte AS cte2 WHERE cte.pList = cte2.pList
      FOR XML PATH(''), TYPE).value(N'./text()[1]',N'varchar(max)'), 1, 2, ''), ''), 
      'No position selected'
  )
  FROM cte
  GROUP BY pList;

EDIT

Here is a slightly different version that seems to obey the order in the original list and relies on a numbers table...

CREATE TABLE dbo.Numbers(n INT PRIMARY KEY);

INSERT dbo.Numbers(n) SELECT TOP 1000 ROW_NUMBER() OVER
 (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1
 CROSS JOIN sys.all_objects AS s2;

...and the OPTION (FORCE ORDER) hint:

;WITH cte AS 
(
    SELECT *, r = ROW_NUMBER() OVER (PARTITION BY pList ORDER BY n.n) 
        FROM @row AS r LEFT OUTER JOIN @pos AS p
    ON ',' + r.pList + ',' LIKE '%,' + RTRIM(p.PositionId) + ',%'
    LEFT OUTER JOIN dbo.Numbers AS n
    ON n.n = CHARINDEX(',' + RTRIM(p.PositionId) + ',', ',' + r.pList + ',')
)
SELECT pList, Position = COALESCE(STUFF((SELECT ',' + PositionName 
    FROM cte AS cte2 INNER JOIN dbo.Numbers AS n ON n.n = cte2.r
    WHERE cte2.pList = cte.pList FOR XML PATH(''), TYPE
    ).value('./text()[1]', N'nvarchar(max)'), 1, 1, ''), 'No position selected')
FROM cte GROUP BY pList OPTION (FORCE ORDER);

This relies on the assumption that the optimizer will choose the clustered primary key on the numbers table. To enforce this further it may make sense to ensure that that index is used by naming it using the WITH (INDEX) hint on both of the joins to dbo.Numbers (which means you should probably name the primary key constraint vs. doing it the lazy way like I did above).

Sorry it took me a bit to come back to this.

Solution 3:

Please try there

--------------sample table ---------------------------------------

create table #tmp_table (PositionId int, PositionName varchar(50))

insert into #tmp_table values (1, 'Defensive End')
insert into #tmp_table values (2, 'Quarterback')
insert into #tmp_table values (3, 'Pitcher')
insert into #tmp_table values (4, 'Catcher')
insert into #tmp_table values (5, 'First Base')

create table #postition (post varchar(20))
insert into #postition values ('1,2,3,')
insert into #postition values ('1,0,0,')
insert into #postition values ('0,0,0,')

----------------------------------stored procedure -------------------------------

-- temp result table
create table #tmp_result (post_list varchar(2000))

DECLARE post_cursor CURSOR 
    STATIC READ_ONLY
    FOR 
    select post     
    from #postition

-- temp variable
declare @post varchar(20)

OPEN post_cursor

FETCH NEXT FROM post_cursor INTO @post

-- remove last comma
SET @post = LEFT(@post,LEN(@post) - 1)

WHILE @@FETCH_STATUS <> -1
BEGIN
    -- do stuff
    exec (
            '
            insert into #tmp_result(post_list)
            SELECT STUFF(
            (
                SELECT '','' + PositionName
                FROM #tmp_table C
                WHERE positionId in (' + @post + ')
                FOR XML PATH('''')
            ), 1, 1, '''') 
        ')
    -- fetch again
    FETCH NEXT FROM post_cursor INTO @post

    SET @post = LEFT(@post,LEN(@post) - 1)  

END

CLOSE post_cursor
DEALLOCATE post_cursor


-- return result
select *
from #tmp_result