How to set multiple output parameters in stored procedures - sql-server

Is there a way to set multiple output parameters?
For example, in the stored procedure shown here, I wish to get the NUM_OF_ROWS and the TicketNumberz from the same stored procedure.
Since, I am new I don't know how to go about it...
DECLARE #TicketNumberz VARCHAR(15) OUT
DECLARE #NUM_OF_ROWS INT OUT
DECLARE #INIT INT=1 OUT
SET #TicketNumberz = (SELECT TICKETNUMBER
FROM TicketHistory s1
WHERE TICKETTIME IN (SELECT MAX(S2.TICKETTIME)
FROM TicketHistory] S2
WHERE s1.TICKETNUMBER = S2.TICKETNUMBER)
AND CURRENTSTATUS_ANALYST != 'Closed'
AND CURRENTSTATUS_ANALYST = 'Resolved'
AND TICKETTIME < GETDATE() - 5)
-- after getting all the list of ticket numbers, update query follows to update the ticket status to 'Closed'
WHILE (#INIT <= #NUM_OF_ROWS)
BEGIN
INSERT INTO TicketHistory (CURRENTSTATUS_ANALYST, TICKETNUMBER,
PREVIOUSSTATUS_ANALYST, TICKETTIME, FIELD, CREATEDBY)
VALUES ('Closed', #TicketNumberz,
'Resolved', CURRENT_TIMESTAMP, 'Status', 'Auto.User')
END
What this query basically does it, it would fetch all the ''Resolved' tickets which are older than 5 days and had not been 'Closed' automatically. So doing it manually through this stored procedure.
But, I am stuck because of the following error :
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
This is how the db looks like :

I would probably rework the subquery in this but not crucial. It seems that perhaps that table structure is a bit of a challenge here but I think you are looking for something along these lines. This would replace all of the code you posted. There is no need for loops or variables of any kind based on what you posted.
--EDIT--
With the clarification from the OP's comment all that need to happen is add the insert above this select
insert into TicketHistory (CURRENTSTATUS_ANALYST,TICKETNUMBER,PREVIOUSSTATUS_ANALYST,TICKETTIME,FIELD,CREATEDBY)
SELECT 'Closed'
, th.TICKETNUMBER
, 'Resolved'
, CURRENT_TIMESTAMP
, 'Status'
, 'Auto.User'
FROM TicketHistory th
WHERE TICKETTIME IN
(
SELECT MAX(S2.TICKETTIME)
FROM TicketHistory S2
WHERE s1.TICKETNUMBER = S2.TICKETNUMBER
)
--AND CURRENTSTATUS_ANALYST != 'Closed' --there is no value that equals 'Resolved' where it could also equal 'Closed'
AND th.CURRENTSTATUS_ANALYST = 'Resolved'
AND th.TICKETTIME < dateadd(day, -5, GETDATE()) --use dateadd instead of shorthand

Finally, I was able to achieve the desired result with the below code :
--Creating a table variable
declare #TempTable table(ticketnum varchar(50))
--Inserting values in table variable using procedure
insert #TempTable
select TICKETNUMBER
FROM TicketHistory th
WHERE TICKETTIME IN
(
SELECT MAX(S2.TICKETTIME)
FROM TicketHistory S2
WHERE th.TICKETNUMBER = S2.TICKETNUMBER
)
AND th.CURRENTSTATUS_ANALYST = 'Resolved'
AND th.TICKETTIME < dateadd(day, -5, GETDATE())
--Selecting values from table variable
SELECT * from #TempTable
DECLARE #ticket_number varchar(100)
DECLARE cur CURSOR FOR SELECT ticketnum FROM #TempTable
OPEN cur
FETCH NEXT FROM cur INTO #ticket_number
WHILE ##FETCH_STATUS = 0
BEGIN
insert into TicketHistory (CURRENTSTATUS_ANALYST,TICKETNUMBER,PREVIOUSSTATUS_ANALYST,TICKETTIME,FIELD,CREATEDBY)
values('Closed', #ticket_number, 'Resolved', CURRENT_TIMESTAMP, 'Status', 'User.Auto')
FETCH NEXT FROM cur INTO #ticket_number
END
CLOSE cur
DEALLOCATE cur
END

Related

Using user defined function withing Update query in SQL Server

Good day!
I've used my user-defined function within an update statement in the following way:
UPDATE Users
SET
CurrentLevel = [dbo].[GetCurrentLevel](id),
Rate = [dbo].[GetRate](id),
LastUpdate = GETUTCDATE()
WHERE id IN (1,2,3)
And it gives me the error:
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression.
I always get only 1 value whenever I execute these functions outside this Update query.
The following code works as expected
DECLARE id_cursor CURSOR LOCAL FORWARD_ONLY FOR
SELECT id FROM Users
WHERE id IN (1,2,3)
OPEN id_cursor;
FETCH NEXT FROM id_cursor
INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #rate FLOAT = [dbo].[GetRate](#id);
UPDATE Users
SET
CurrentLevel = [dbo].[GetCurrentLevel](#id),
Rate = #rate,
LastUpdate = GETUTCDATE()
WHERE id = #id
FETCH NEXT FROM id_cursor
INTO #id
END
I do not understand why am I forced to use a cursor and update 1 row at time.
UPDATE.
This is not a trigger, but a stored procedure.
The user defined-functions both have return type FLOAT, which means that can not return multiple values by definition. Function code:
CREATE FUNCTION [dbo].[GetRate]
(
#userID int
)
RETURNS FLOAT
AS
BEGIN
DECLARE #discount INT = (SELECT TOP 1 [BonusSize] FROM Bonuses WHERE userID = #userID);
DECLARE #rate FLOAT = (SELECT SUM(s.Level * (i.Bonus + #discount))
FROM [Stats] s
JOIN [Orders] v
ON s.Level > 0 AND s.id = v.id AND v.userID = #userID
JOIN (SELECT id, SUM(ISNULL(Bonus,0)) as Bonus FROM Items GROUP BY id) i
ON i.id = v.id)
RETURN ISNULL(#rate/86400.0, 0);
END

Stored procedure only executes correctly on first execution

I've written this SQL Server stored procedure that inserts records into another table based on the order frequency of customers in another table. It assigns a rank to each customer based on their order frequency. When I create the procedure and execute it for the first time, it works fine and inserts the correct records into the table. But when I clear the table and try to execute the procedure again, no records are added. I have to delete the procedure, restart SSMS, and create the procedure again for it to work correctly again.
Here is the procedure:
create procedure TopKCustomer (#CustRank decimal(11,0))
as
declare CustCursor cursor local for
select o.CustomerID,c.CustomerName,c.CustomerPostalCode,
count(o.CustomerID) as 'Order Frequency'
from (Customer_T c join Order_T o on c.CustomerID=o.CustomerID)
group by o.CustomerID,c.CustomerName,c.CustomerPostalCode
order by [Order Frequency] desc;
declare #PrevOrderFreq float;
declare #CurrOrderFreq float;
declare #CurrRank decimal(11,0);
declare #CurrCustID decimal(11,0);
declare #CurrCustName varchar(25);
declare #CurrCustPostCode varchar(10);
begin
set #PrevOrderFreq = 0;
set #CurrOrderFreq = 0;
set #CurrRank = 0;
set #CurrCustID = 0;
set #CurrCustName = '';
set #CurrCustPostCode = '';
open CustCursor;
while ##FETCH_STATUS = 0
begin
fetch next from CustCursor into #CurrCustID, #CurrCustName, #CurrCustPostCode, #CurrOrderFreq;
if #CurrOrderFreq <> #PrevOrderFreq
begin
set #CurrRank = (#CurrRank + 1);
if #CurrRank > #CustRank
begin
break;
end
end
insert into TopKCustomer_T
values (#CurrCustID, #CurrCustName, #CurrCustPostCode, #CurrRank, getdate());
set #PrevOrderFreq = #CurrOrderFreq;
end
close CustCursor;
deallocate CustCursor;
end
Here are the tables I'm working with:
Customer_T (CustomerID, CustomerName, CustomerAddress, CustomerCity, CustomerState, CustomerPostalCode)
Order_T (OrderID, CustomerID, OrderDate)
TopKCustomer (CustomerID, CustomerName, CustomerPostalCode, CRank, RankGenerateDate)
I think the problem is
while ##FETCH_STATUS = 0
This will be result of the previous fetch (in other words the fetch from the previous execution of your stored procedure, not what you want).
The usual way I wrote cursor loops is
while 1 =1
begin
fetch next from c into ...
if ##fetch_status != 0 break
...
end
There's no sample data or table structure so I don't know what your data looks like. Below is what I think you want. The inner query count the order per customer. The outer query rank them.
SELECT *
, DENSE_RANK() OVER(PARTITION BY CustomerID ORDER BY OrderFrequency) AS Rnk
FROM (
SELECT *
, COUNT(*) OVER (PARTITION BY o.CustomerID) AS OrderFrequency
FROM Customer_T c
JOIN Order_T o ON c.CustomerID = o.CustomerID
) a

SQL server Function - Take column name as input parameter

I need to write a SQL function to return column specific values, so I am passing the column name as a parameter to SQL-function to return its corresponding value. Here is the sample function
CREATE FUNCTION GETDATETIME(#columnName VARCHAR(100))
RETURNS DATETIME
AS
BEGIN
RETURN (SELECT TOP 1.#columnName FROM TEST_TABLE )
END
GO
The above function seems to be straight forward, but it not working as expected.
And when I execute the function
SELECT dbo.GETDATETIME('DATETIMECOLUMNNAME')
I am getting this error:
Conversion failed when converting date and/or time from character string.
Can someone help me to identify the issue?
For that you need to write dynamic sql. But Functions won't support execute statement.
So you need to write multiple If conditions for each column.
CREATE FUNCTION GETDATETIME(#columnName VARCHAR(100))
RETURNS DATETIME
AS
BEGIN
DECLARE #RESULT DATETIME;
IF (#columnName = 'ABC')
Begin
SELECT TOP 1 #RESULT = [ABC] FROM TEST_TABLE
END
ELSE IF (#columnName = 'DEF')
Begin
SELECT TOP 1 #RESULT = [DEF] FROM TEST_TABLE
END
ELSE IF (#columnName = 'GHI')
Begin
SELECT TOP 1 #RESULT = [GHI] FROM TEST_TABLE
END
RETURN #RESULT
END
GO
Edit 2:
If your column always return Datetime, then you can do like below.
CREATE TABLE A_DUM (ID INT, STARTDATE DATETIME, ENDDATE DATETIME, MIDDLEDATE DATETIME)
INSERT INTO A_DUM
SELECT 1, '2019-07-24 11:35:58.910', '2019-07-28 11:35:58.910', '2019-07-26 11:35:58.910'
UNION ALL
SELECT 2, '2019-07-29 11:35:58.910', '2019-08-01 11:35:58.910', '2019-07-24 11:35:58.910'
And your function like below
CREATE FUNCTION GETDATETIME(#columnName VARCHAR(100))
RETURNS DATETIME
AS
BEGIN
DECLARE #RESULT DATETIME;
SELECT TOP 1 #RESULT = CAST(PROP AS DATETIME)
FROM A_DUM
UNPIVOT
(
PROP FOR VAL IN (STARTDATE, ENDDATE,MIDDLEDATE)
)UP
WHERE VAL = #columnName
RETURN #RESULT
END
GO
There's a workaround to this, similar to #Shakeer's answer - if you are attempting to GROUP BY or perform a WHERE on a column name, then you can just use a CASE statement to create a clause to match on specific column names (if you know them).
Obviously this doesn't work very well if you have many columns to hard-code, but at least it's a way to achieve the general idea.
E.g. with WHERE clause:
WHERE
(CASE
WHEN #columnname = 'FirstColumn' THEN FirstColumnCondition
WHEN #columnname = 'SecondColumn' THEN SecondColumnCondition
ELSE SomeOtherColumnCondition
END)
Or with GROUP BY:
GROUP BY
(CASE
WHEN #columnname = 'FirstColumn' THEN FirstColumnGroup
WHEN #columnname = 'SecondColumn' THEN SecondColumnGroup
ELSE SomeOtherColumnGroup
END)
No you cannot use dynamic sql in functions in SQL. Please check this link for more info link.
So it is not possible to achieve this by any function, yes you may use stored procedures with output parameter for same.
You may find this link for reference link.

Running tsqlt assert inside a cursor

I'm writing tsqlt against a proc that can be run against various parameter values. I initially built a proc that populates fake tables - and then 1 tsqlt test per possible value (ended up with 35 tests and they each worked).
What I would like to do is reduce these into 1 test (since they are all really testing the same functionality - just for different values). I thought I could do this with a cursor like so:
---- Declare Sproc Variables
DECLARE #ReviewId INT;
DECLARE #SourceId INT = 1;
CREATE TABLE #Present
(
SubmissionReviewId INT ,
username VARCHAR(50)
);
CREATE TABLE #Expected
(
SubmissionReviewId INT ,
username VARCHAR(50)
);
--Create Cursor to loop through each active value
DECLARE review_id CURSOR
FOR
SELECT ReviewId
FROM reftype.Rev
WHERE IsActive = 1;
OPEN review_id;
FETCH NEXT FROM review_id
INTO #ReviewId;
WHILE ##FETCH_STATUS = 0
BEGIN
--Setup Fake Data according to the specified test condition
EXEC ut_DataSetupProc #ReviewId = #ReviewId;
-- Run set cutover Sproc
EXEC Procbeing Tested #ReviewId = #ReviewId,
#SourceId = 1, #Username = 'blah';
-- Confirm appropriate review is present in Submission Review Active
DELETE FROM #Present;
DELETE FROM #Expected;
INSERT INTO #Present
SELECT SubmissionReviewId ,
LastModifiedBy
FROM review.SubmissionReviewActive
ORDER BY SubmissionReviewId ,
LastModifiedBy;
/**********************Create table holding expected values***************************/
INSERT INTO #Expected
--This confirms active reviews that belong to other sections/sources remain unaffected
SELECT SubmissionReviewId ,
LastModifiedBy
FROM review.SubmissionReviewActive
WHERE ( ReviewId != #ReviewId )
OR ( SourceId != #SourceId )
UNION
SELECT sra.SubmissionReviewId ,
sra.LastModifiedBy
FROM review.SubmissionReviewActive sra
JOIN review.SubmissionReviewFutureActive srfa ON srfa.IssuerId = sra.IssuerId
AND srfa.ReviewId = sra.ReviewId
AND srfa.Version < sra.Version
WHERE sra.ReviewId = #ReviewId
AND sra.SourceId = #SourceId
UNION
SELECT srfa.SubmissionReviewId ,
'jmarina' AS LastModifiedBy
FROM review.SubmissionReviewFutureActive srfa
JOIN review.SubmissionReviewActive sra ON srfa.IssuerId = sra.IssuerId
AND srfa.ReviewId = sra.ReviewId
AND srfa.Version > sra.Version
WHERE sra.ReviewId = #ReviewId
AND srfa.SourceId = #SourceId
UNION
SELECT srfa.SubmissionReviewId ,
'blah' AS LastModifiedBy
FROM review.SubmissionReviewFutureActive srfa
WHERE srfa.ReviewId = #ReviewId
AND srfa.SourceId = #SourceId
AND srfa.IssuerId NOT IN (
SELECT IssuerId
FROM review.SubmissionReviewActive
WHERE ReviewId = #ReviewId
AND SourceId = #SourceId )
UNION
SELECT sra.SubmissionReviewId ,
sra.LastModifiedBy
FROM review.SubmissionReviewActive sra
WHERE sra.ReviewId = #ReviewId
AND sra.SourceId = #SourceId
AND IssuerId NOT IN (
SELECT IssuerId
FROM review.SubmissionReviewFutureActive
WHERE ReviewId = #ReviewId
AND SourceId = #SourceId )
ORDER BY SubmissionReviewId ,
LastModifiedBy;
/*************************************************************/
EXEC tSQLt.AssertEqualsTable #Expected = '#Expected',
#Actual = '#Present', #Message = N'', -- nvarchar(max)
#FailMsg = N'Active Status is not a match'; -- nvarchar(max)
FETCH NEXT FROM review_id
INTO #ReviewId;
END;
CLOSE review_id;
DEALLOCATE review_id;
DROP TABLE #Expected;
DROP TABLE #Present;
END;
However, running this using
EXEC proc name #ReviewId = #ReviewId;
yields a message saying no tests were run. How can I sue a cursor to reduce my number of tests? Or is there another approach I should consider?
I'd suggest you write something called a parameterized test.
tSQLt does not (yet) have native support for that, but there is an easy workaround:
You start by writing one of your tests normally. But instead of hardcoding the pertinent values, you make them parameters of the procedure. (For data sets, you can use table parameters.)
You also name that procedure something that doesn't start with "test" (but lives in the same schema).
Then you write one real test per actual case, each one consisting of one line: the execution of your parameterized procedure.
That will lead to tests that are a lot easier to understand than your current approach. And additionally, if one of them fails, you immediately know which.
As a side note: You always want to hardcode your expected results. Your current code is way complex. You want to minimize things that can go wrong in the test itself. Really, your goal should be tests that can be understood with one glance.
In the end I achieved the end goal in a couple of steps:
1. Move the assert statement outside of the cursor
2. Created 'cased' temp table with pass/fail records
INSERT INTO #ActualAssert
SELECT p.SubmissionReviewId,e.SubmissionReviewId,
CASE WHEN ( e.SubmissionReviewId IS NULL
OR p.SubmissionReviewId IS NULL
) THEN 'Fail'
ELSE 'Pass'
END
FROM #Present p
LEFT JOIN #Expected e ON e.SubmissionReviewId = p.SubmissionReviewId
UNION
SELECT p.SubmissionReviewId,e.SubmissionReviewId ,
CASE WHEN ( e.SubmissionReviewId IS NULL
OR p.SubmissionReviewId IS NULL
) THEN 'Fail'
ELSE 'Pass'
END
FROM #Present p
RIGHT JOIN #Expected e ON e.SubmissionReviewId = p.SubmissionReviewId;
3. Outside of the cursor I set up a new parameter that takes any fails if they exist or 'pass' if they don't
SET #Result = ( SELECT DISTINCT TOP 1
TestStatus
FROM #ActualAssert
ORDER BY TestStatus ASC
);
4. Then I modified the assert to fail if #result is anything other than 'Pass'
EXEC tSQLt.AssertEqualsString #Expected = N'Pass', -- nvarchar(max)
#Actual = #Result, #Message = N''; -- nvarchar(max)
** A note I change previous present and expected temp tables into variable tables

T-SQL different result between code in stored and same code in query pane

I'm working on a procedure that should return a o or a 1, depending on result from parameter calculation (parameters used to interrogate 2 tables in a database).
When I excute that code in a query pane, it gives me the results i'm expecting.
code looks like:
SELECT TOP 1 state, updDate INTO #history
FROM [xxx].[dbo].[ImportHystory] WHERE (db = 'EB') ORDER BY addDate DESC;
IF (SELECT state FROM #history) = 'O'
BEGIN
SELECT TOP 1 * INTO #process_status
FROM yyy.dbo.process_status WHERE KeyName = 'eb-importer';
IF(SELECT s.EndDate FROM #process_status s) IS NOT NULL
IF (SELECT s.EndDate FROM #process_status s) > (SELECT h.updDate FROM #history h)
BEGIN
IF (SELECT MessageLog from #process_status) IS NOT NULL SELECT 1;
ELSE SELECT 0;
END
ELSE
SELECT 1;
ELSE
SELECT 1;
END
ELSE
SELECT 0
I'm in the situation where EndDate from #process_status is null, so the execution returns 1.
Once i put the SAME code in a SP, and pass 'EB' and 'eb-importer' as parameters, it returns 0.
And I exec the procedure with the data from the table right in front of me, so i know for sure that result is wrong.
Inside the procedure:
ALTER PROCEDURE [dbo].[can_start_import] (#keyName varchar, #db varchar, #result bit output)
DECLARE #result bit;
and replace every
SELECT {0|1}
with
SELECT #result = {0|1}
Executed from the Query pane:
DECLARE #result bit;
EXEC [dbo].[can_start_import] #KeyName = 'eb-importer', #db = 'EB', #result = #result OUTPUT
SELECT #result AS N'#result'
Why does this happen?
You are doing a top(1) query without an order by. That means SQL Server can pick any row from table1 that matches the where clause.
If you want to guarantee that the result is the same every time you execute that code you need an order by statement that unambiguously orders the rows.
So, apparently 2 things needed to be done:
set the length of the varchar parameter with a higher length,
filter with ' like ' instead of ' = ' for god knows what reason
Now it work as i expected to do, but i still don't get the different results between the query pane and the procedure if i use the equal...

Resources