Creating error record when SQL Server udf function returns a certain result - sql-server

I have been wrecking my brain over this all weekend.
I have a SQL Server UDF to check whether a value passed as an argument will truncate.
CREATE FUNCTION willTruncate
(#fldValue nvarchar(200),
#fldSize smallint,
#fieldname nvarchar(40),
#tbl varchar(15))
RETURNS varchar(200)
AS
BEGIN
declare #Return varchar(200)
declare #fldLen smallint
SET #fldLen = len(#fldValue)
if #fldLen <= #fldSize
SET #Return = #fldValue
else
SET #Return = left(#fldValue,#fldSize-3) + '...'
RETURN #Return
END
The function works correctly to truncate the data the way I need it to when I use it in a SQL query like the sample below:
select top 10
'test' as db,
fname,
dbo.willTruncate(lname, 5, 'Last Name', 'People') as lastname,
dbo.willTruncate(address,40,'Address Line 1', 'People') as addressLine1
from
people
My problem is that I need to report back in some manner about the truncations so the end users can correct the record.
I have a table to populate and have tried to insert a record as part of the else clause, a separate function and a stored procedure. Each time I run the code it fails.
insert into tblTruncationErrors(trunkTable, trunkField, trunkValue)
values(#tbl, #fieldname, #fldValue)
or
CREATE FUNCTION dbo.logTruncation
(#fldValue nvarchar(200), #fieldname nvarchar(40), #tbl varchar(15))
RETURNS varchar(30)
AS
BEGIN
declare #Return int
insert into dbo.truncationErrors(trunkTable, trunkField, trunkValue)
Values (#tbl, #fieldname, #fldValue)
SET #Return = #fieldname
RETURN #Return
END
or
CREATE PROC sp_logTruncation
#tbl varchar(20),
#fldName varchar(30),
#fldValue varchar(200)
AS
SET NOCOUNT ON
insert into dbo.truncationErrors(trunkTable, trunkField, trunkValue)
Values (#tbl, #fldName, #fldValue)
SET NOCOUNT OFF
RETURN 1
GO
Running it as part of the original function code fails saying it needs to be done in a function or stored procedure. If I do it as a function it says I must use a stored procedure. Using it as a stored procedure results in a message saying it can only run from a function.
So, am I writing this wrong? Is there a way to do this. I am not insistent about it going into a table so a Excel or csv file output would be wonderful as well.

Related

When exactly do we use stored procedures with output parameters?

When exactly do we use stored procedures with output parameters and when do we use stored procedures without parameters?
I base my question on an example:
Stored procedure with output parameter
CREATE PROCEDURE uspGetContactsCountByCity
#City nvarchar(60),
#ContactsCount int OUT
AS
BEGIN
SELECT #ContactsCount = COUNT(ContactID)
FROM Contacts
WHERE City = #City
END
Stored procedure executing
DECLARE #ContactsTotal INT
EXEC uspGetContactsCountByCity #ContactsCount = #ContactsTotal OUT, #city = 'Berlin'
SELECT #ContactsTotal
Results: 2
Stored procedure without output parameter
CREATE PROCEDURE uspGetContactsCountByCity2
#City nvarchar(60)
AS
BEGIN
SELECT COUNT(ContactID)
FROM Contacts
WHERE City = #City
END
Stored procedure executing:
EXEC uspGetContactsCountByCity2 #city = 'Berlin'
Results: 2
Both procedures return the same result, in same form, so what's the difference?
Basically, the result you're seeing is actually the result of your SELECT at the end of the procedure, which is doing the same thing.
Please take a look at this documentation:
If you specify the OUTPUT keyword for a parameter in the procedure definition, the stored procedure can return the current value of the parameter to the calling program when the stored procedure exits. To save the value of the parameter in a variable that can be used in the calling program, the calling program must use the OUTPUT keyword when executing the stored procedure.
So basically if you would like your stored procedure to just return just a value instead of a data set, you could use the output parameter. For example, let's take the procedures you have given as an example. They both do the same thing, this is why you got the same result. But what about changing a little bit in the first procedure that has the output parameter.
Here's an example:
create table OutputParameter (
ParaName varchar(100)
)
insert into OutputParameter values ('one'), ('two'),('three'),('one')
CREATE PROCEDURE AllDataAndCountWhereOne
#name nvarchar(60),
#count int OUT
as
Begin
SELECT #count = COUNT(*) from OutputParameter
Where ParaName = #name
select Distinct(ParaName) from OutputParameter
End
Declare #TotalCount int
Exec AllDataAndCountWhereOne #count = #TotalCount OUT, #name = 'One'
Select #TotalCount
With this example, you are getting all the distinct stored data in the table, plus getting the count of a given name.
ParaName
--------------------
one
three
two
(3 row(s) affected)
-----------
2
(1 row(s) affected)
This is one way of using the output parameter. You got both the distinct data and the count you wanted without doing extra query after getting the initial data set.
At the end, to answer your question:
Both procedures gives us the same result, in same form, so what's the difference?
You didn't make a difference in your own results, this is why you didn't really notice the difference.
Other Examples:
You could use the OUT parameter in other kinds of procedures. Let's assume that your stored procedure doesn't return anything, it's more like a command to the DB, but you still want a kind of message back, or more specifically a value. Take these two examples:
CREATE PROCEDURE InsertDbAndGetLastInsertedId
--This procedure will insert your name in the database, and return as output parameter the last inserted ID.
#name nvarchar(60),
#LastId int OUT
as
Begin
insert into OutputParameterWithId values (#name);
SELECT #LastId = SCOPE_IDENTITY()
End
or:
CREATE PROCEDURE InsertIntoDbUnlessSomeLogicFails
--This procedure will only insert into the db if name does exist, but there's no more than 5 of it
#name nvarchar(60),
#ErrorMessage varchar(100) OUT
as
Begin
set #ErrorMessage = ''
if ((select count(*) from OutputParameterWithId) = 0)
begin
set #ErrorMessage = 'Name Does Not Exist'
return
end
if ((select count(*) from OutputParameterWithId) = 5)
begin
set #ErrorMessage = 'Already have five'
return
end
insert into OutputParameterWithId values (#name);
End
These are just dummy examples, but just to make the idea more clear.
An example, based on yours would be if you introduced paging to the query.
So the result set is constrained to 10 items, and you use a total count out parameter to drive paging on a grid on screen.
Answer from ozz regarding paging does not make sense because there is no input param that implements a contraint on the number of records returned.
However, to answer the question... the results returned by these stored procedures are not the same. The first returns the record count of contacts in given city in the out param ContactsCount. While the count may also be recieved in the second implement through examining the reader.Rows.Count, the actual records are also made a available. In the first, no records are returned - only the count.

SQL Server : parameterized stored procedure

I have to do this in SQL Server. Assume that I have 2 tables.
Based on parameters Name and Surname, I have to take PhysicianID from Table1.
After that I have to create new record using insert into stored procedure.
Something like this
CREATE PROCEDURE FIND_PHYSICIANID
#FirstName varchar(50),
#LastName varchar(50)
AS
BEGIN
DECLARE #PhysicianID int
SELECT #PhysicianID = PhysicianID
FROM Table1
WHERE FirstName = #FirstName AND LastName = #LastName
RETURN #PhysicianID
END
EXECUTE FIND_PHYSICIANID 'Kathlin','Jones'
CREATE PROCEDURE ADD_APPOINTMENT -- Create a new appointment
#AppointmentType VARCHAR(70), --Type of new appointment
#pAppointmentDate DATE, -- Date of new appointment
#aPhysicianID INT, --PhysicianID of requested physician (in this case during execution we will take value which we know-read from table for requested first and last name)
#apPatientID INT, --PatientID of chosen patient(let's say any from 1 to 14)
#aScheduleID INT, --ScheduleID, but here we have to take some ScheduleID for chosen PhysicianID (in this case during execution we will take value which we know-based on PHYSICIANID we may read value from table SCHEDULE)
#Status CHAR(1) -- Just Y or N
AS -- This "AS" is required by the syntax of stored procedures.
BEGIN -- Insert the new appointment
INSERT INTO [APPOINTMENT]([AppointmentType], [AppointmentDate],[aPhysicianID],
[apPatientID], [aScheduleID], [Status-Canceled])
VALUES (#AppointmentType, #pAppointmentDate, #aPhysicianID,
#apPatientID, #aScheduleID, #Status);
END;
EXECUTE ADD_APPOINTMENT 'Vaccinations', '2017-0831', '#PhysicianID', '12', '289', 'N'
You can get return id like this.
DECLARE #PhysicianID int
EXECUTE #PhysicianID = FIND_PHYSICIANID 'Kathlin','Jones'
you can use this param like this
EXECUTE ADD_APPOINTMENT 'Vaccinations','2017-0831', #PhysicianID, '12','289','N'
Presuming that the ability to find a physician is a common operation you could convert the FIND_PHYSICIANID stored procedure to a function and delay the lookup to within the consuming stored procedure that performs the operation.
create function [dbo].[FIND_PHYSICIANID]
(
#FirstName varchar(50),
#LastName varchar(50)
)
returns int
as
begin
declare #PhysicianId int
select #PhysicianID = PhysicianID
from dbo.Table1
where FirstName = #FirstName
and LastName = #LastName
return #PhysicianId
end
This will still keep the logic of finding a physician centralised but allow you to perform other actions and possibly validation if the only information you have available to you is the full name. Yes, it is more parameters but this is assuming the required parameters for the stored procedures are a manageable amount.
create procedure [dbo].[ADD_APPOINTMENT] -- Create a new appointment
#AppointmentType VARCHAR(70), --Type of new appointment
#pAppointmentDate DATE, -- Date of new appointment
#PhysicianFirstName varchar(50), -- // The first name of the physician
#PhysicianLastName varchar(50), -- // The last name of the physician
#apPatientID INT, --PatientID of chosen patient(let's say any from 1 to 14)
#aScheduleID INT, --ScheduleID, but here we have to take some ScheduleID for chosen PhysicianID (in this case during execution we will take value which we know-based on PHYSICIANID we may read value from table SCHEDULE)
#Status CHAR(1) -- Just Y or N
AS -- This "AS" is required by the syntax of stored procedures.
BEGIN -- Insert the new appointment
declare #aPhysicianID int
select #aPhysicianID = [dbo].[FIND_PHYSICIANID](#PhysicianFirstName, #PhysicianLastName varchar(50))
INSERT INTO [APPOINTMENT]([AppointmentType], [AppointmentDate],[aPhysicianID],
[apPatientID], [aScheduleID], [Status-Canceled])
VALUES (#AppointmentType, #pAppointmentDate, #aPhysicianID,
#apPatientID, #aScheduleID, #Status);
END
Alternatively, if it is desired to be separated and keep the existing stored procedure parameter signature then the previous answer that has the caller lookup the physician id via the stored procedure locally and then pass that parameter into the add appointment stored procedure should suffice your requirements.
As per the below pseudocode
Declare a variable for #physicianid of type int
Assign the #physicianid variable to the output of the FIND_PHYSICIANID stored procedure
Execute ADD_APPOINTMENT stored procedure with #physicianid variable as an input

calling stored proc from stored proc with table-valued parameters

I have a stored procedure (sp_A) that receives data through a table-valued parameter (tvp), inserts all records into a table (table_A), and then makes a call to another stored procedure (sp_B). sp_B receives input parameters from some of the columns in the tvp, and has one output parameter #return of type nvarchar. Executing sp_B with a single row of input parameters works just fine. The problem is in calling sp_B from sp_A with a tvp, because then sp_B returns more than one value, which I'd like to handle, but can't find a suitable data structure to hold all the return values.
I have here a semblance of the problematic query
CREATE PROCEDURE [dbo].[sp_A]
#return varchar(50) output,
#inputData inputTable readonly
AS
BEGIN TRANSACTION
SET NOCOUNT OFF;
declare #typeid int, #insertcount int
set #typeid = (select typeid from #inputData )
--if typeid is not provided, insert into table_A and exit stored proc
if(#typeid IS NULL OR #typeid = 0)
BEGIN
INSERT INTO [dbo].[Table_A]
[cola1], [cola2], [cola3])
select _cola1, _cola2, _cola3
from #inputData
set #insertcount = ##ROWCOUNT
set #return = 'successful'
END
--else if typeid is provided, insert into table_A, and call sp_B, passing input parameters from #inputData
ELSE
BEGIN
declare #colb1 varchar, #colb2 varchar, #colb3 varchar
INSERT INTO [dbo].[Table_A]
([cola1], [cola2], [cola3], [typeid ])
select _cola1, _cola2, _cola3, _typeid
from #inputData
select #colb1 = _colb1, #colb2 = _colb2, #colb3 = _colb3
from #moTable
exec #insertcount = sp_B
#colb1, #colb2, #colb3, #return output
END
COMMIT TRANSACTION
select #insertcount
RETURN
I'm getting a "subquery returns more than 1 value..." error, understandably so... but I need that full value set for further processing in the application. What would be the best way to return, handle and propagate the return value set to the calling program (C# in this case)?

Null values stored in table via Stored Procedure

Following is my stored procedure which stores data in two tables namely SuccessfulLogins and FailedLogins
ALTER procedure [dbo].[Proc_CheckUser]
#UserID VARCHAR(50),
#Password VARCHAR(50)
AS
SET NOCOUNT ON
DECLARE #ReturnVal VARCHAR(500)
DECLARE #PasswordOld VARCHAR(50)
DECLARE #Type NVARCHAR(50)
DECLARE #IP NVARCHAR(50)
SELECT #PasswordOld = Password,#Type=ClientType,#IP=IPAddress
FROM Clients
WHERE Username = #userid
IF (#PasswordOld IS NULL)
BEGIN
SET #ReturnVal='1|Incorrect Username'
INSERT INTO FailedLogins(Username,Password,ClientType,Reason,IPAddress)
VALUES(#UserID,Hashbytes('SHA1',#Password),#Type,'Invalid Username',#IP)
END
ELSE
BEGIN
IF (#PasswordOld!=Hashbytes('SHA1',#Password))
BEGIN
SET #ReturnVal='1|Incorrect Password'
INSERT INTO FailedLogins(Username,Password,ClientType,Reason,IPAddress)
VALUES(#UserID,Hashbytes('SHA1',#Password),#Type,'Invalid Password',#IP)
END
ELSE
BEGIN
SET #ReturnVal='0|Logged in Successfully' +'|'+ rtrim(cast(#Type as char))
INSERT INTO SuccessfulLogins(Username,Password,ClientType,Reason,IPAddress)
VALUES(#UserID,Hashbytes('SHA1',#Password),#Type,'Valid Login Credentials Provided',#IP)
END
END
SELECT #ReturnVal
The problem here is that whenever I enter an Invalid Username,the stored procedure returns the correct message ie Incorrect Username but it stores NULL values in the fields ClientType and IPAddress in Failed Logins Table
Following is my insert query for Invalid username
IF (#PasswordOld!=Hashbytes('SHA1',#Password))
BEGIN
SET #ReturnVal='1|Incorrect Password'
INSERT INTO FailedLogins(Username,Password,ClientType,Reason,IPAddress)
VALUES(#UserID,Hashbytes('SHA1',#Password),#Type,'Invalid Password',#IP)
END
Can anyone help me to rectify this.How to check condition for username?
Thanks
Your code reads
SELECT #PasswordOld = Password,#Type=ClientType,#IP=IPAddress
FROM Clients
WHERE Username = #userid
Wouldn't this mean that no row will be returned for a Username that does not exist? So, the values for ClientType and IPAddress will not get populated and will remain NULL, which would be the expected functionality.
However, if you want to store some value, or these fields are not nullable, assign a static value to these parameters.
Your query is correct. When there is no match for the Username = #UserId , The #Type , #IP variables will be null. Since there is no record in the table for that UserName. What you can do is that in the declaration you can initiate to some default value,so that it will be inserted to table FailedLogins.
DECLARE #Type NVARCHAR(50)="DefaultType/NoType"
DECLARE #IP NVARCHAR(50)="0.0.0.1"
Something like the above.
If the username is invalid it does not appear in the table Clients so your fields pulled from that table will also be NULL. To negate this you could decide to use default values for ClientType and IPAddress using static values in your declarations, but storing this would just be obsolete data and I would think changing the structure of FailedLogins to not store this would seem more logical.

INSERT INTO with exec with multiple result sets

SQL Server allows me to insert the returned result set of a stored procedure as:
DECLARE #T TABLE (
ID int,
Name varchar(255),
Amount money)
INSERT INTO #T
exec dbo.pVendorBalance
This works as long as the stored procedure only returns 1 result set.
Is there a way to make this work if the stored procedure returns several result sets?
E.g.
DECLARE #T1 (...)
DECLARE #T2 (...)
INSERT INTO #T1 THEN INTO #T2
exec dbo.pVendorBalance
One workaround to this problem is using OUTPUT parameters (JSON/XML) instead of resultsets.
CREATE TABLE tab1(ID INT, Name NVARCHAR(10), Amount MONEY);
INSERT INTO tab1(ID, Name, Amount)
VALUES (1, 'Alexander', 10),(2, 'Jimmy', 100), (6, 'Billy', 20);
CREATE PROCEDURE dbo.pVendorBalance
AS
BEGIN
-- first resultset
SELECT * FROM tab1 WHERE ID <=2;
-- second resultset
SELECT * FROM tab1 WHERE ID > 5;
END;
Version with OUT params:
CREATE PROCEDURE dbo.pVendorBalance2
#resultSet1 NVARCHAR(MAX) OUT,
#resultSet2 NVARCHAR(MAX) OUT
AS
BEGIN
SELECT #resultSet1 = (SELECT * FROM tab1 WHERE ID <=2 FOR JSON AUTO),
#resultSet2 = (SELECT * FROM tab1 WHERE ID > 5 FOR JSON AUTO);
END;
And final call:
DECLARE #r1 NVARCHAR(MAX), #r2 NVARCHAR(MAX);
EXEC dbo.pVendorBalance2 #r1 OUT, #r2 OUT;
-- first resultset as table
SELECT *
INTO #t1
FROM OpenJson(#r1)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
-- second resultset as table
SELECT *
INTO #t2
FROM OpenJson(#r2)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
SELECT * FROM #t1;
SELECT * FROM #t2;
DBFiddle Demo
EDIT:
Second approach is to use tSQLt.ResultSetFilter CLR function (part of tSQLt testing framework):
The ResultSetFilter procedure provides the ability to retrieve a single result set from a statement which produces multiple result sets.
CREATE TABLE #DatabaseSize (
database_name nvarchar(128),
database_size varchar(18),
unallocated_space varchar(18)
);
CREATE TABLE #ReservedSpaceUsed (
reserved VARCHAR(18),
data VARCHAR(18),
index_size VARCHAR(18),
unused VARCHAR(18)
);
INSERT INTO #DatabaseSize
EXEC tSQLt.ResultSetFilter 1, 'EXEC sp_spaceused';
INSERT INTO #ReservedSpaceUsed
EXEC tSQLt.ResultSetFilter 2, 'EXEC sp_spaceused';
SELECT * FROM #DatabaseSize;
SELECT * FROM #ReservedSpaceUsed;
No. But there is more of a work around since you cannot do an insert into with a procedure that returns multiple results with a different number of columns.
If you are allowed to modify the stored procedure, then you can declare temp tables outside of the procedure and populate them within the stored procedure. Then you can do whatever you need with them outside of the stored procedure.
CREATE TABLE #result1(Each column followed by data type of first result.);
----Example: CREATE TABLE #result1(Column1 int, Column2 varchar(10))
CREATE TABLE #result2(Each column followed by data type of second result.);
EXEC pVendorBalance;
SELECT * FROM #result1;
SELECT * FROM #result2;
I had a similar requirement, and ended up using the a CLR function which you can read about here (it's the answer with the InsertResultSetsToTables method, by user Dan Guzman):
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/da5328a7-5dab-44b3-b2b1-4a8d6d7798b2/insert-into-table-one-or-multiple-result-sets-from-stored-procedure?forum=transactsql
You need to create a SQL Server CLR project in Visual Studio to get going. I had a project already written by a co-worker that I could just expand, but if you're starting from scratch, try reading this guide:
http://www.emoreau.com/Entries/Articles/2015/04/SQL-CLR-Integration-in-2015-year-not-product-version.aspx
If you've succeeded in writing and publishing the CLR project to the database, here is an example of using it I wrote:
-- declare a string with the SQL you want to execute (typically an SP call that returns multiple result sets)
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'exec usp_SomeProcedure #variable1 = ' + #variable1 + '...' -- piece together a long SQL string from various parameters
-- create temp tables (one per result set) to hold the output; could also be actual tables (non-temp) if you want
CREATE TABLE #results_1(
[CustomerId] INT, [Name] varchar(500), [Address] varchar(500)
);
CREATE TABLE #results_2(
[SomeId] UNIQUEIDENTIFIER, [SomeData] INT, [SomethingElse] DateTime
);
-- on the exemplary 'CustomerDatabase' database, there is an SP (created automatically by the SQL CLR project deployment process in Visual Studio) which performs the actual call to the .NET assembly, and executes the .NET code
-- the CLR stored procedure CLR_InsertResultSetsToTables executes the SQL defined in the parameter #sourceQuery, and outputs multiple result sets into the specified list of tables (#targetTableList)
EXEC CustomerDatabase.dbo.CLR_InsertResultSetsToTables #sourceQuery = #sql, #targetTableList = N'#results_1,#results_2';
-- The output of the SP called in #sql is now dumped in the two temp tables and can be used for whatever in regular SQL
SELECT * FROM #results_1;
SELECT * FROM #results_2;
We can do it in the following way
Consider the input SP (which returns 2 tables as output) as usp_SourceData
Alter the usp_SourceData to accept a parameter as 1 and 2
Adjust the SP in a way that when
usp_SourceData '1' is executed it will return first table
and when
usp_SourceData '2' is executed it will return second table.
Actually stored procedures can return multiple result sets, or no result sets, it's pretty arbitrary. Because of this, I don't know of any way to navigate those results from other SQL code calling a stored procedure.
However, you CAN use the returned result set from a table-valued user defined function. It's just like a regular UDF, but instead of returning a scalar value you return a query result. Then you can use that UDF like any other table.
INSERT INTO #T SELECT * FROM dbp.pVendorBalanceUDF()
http://technet.microsoft.com/en-us/library/ms191165(v=sql.105).aspx
DROP TABLE ##Temp
DECLARE #dtmFrom VARCHAR(60) = '2020-12-01 00:00:00', #dtmTo VARCHAR(60) = '2020-12-02 23:59:59.997',#numAdmDscTransID VARCHAR(60) =247054
declare #procname nvarchar(255) = 'spGetCashUnpaidBills',
#procWithParam nvarchar(255) = '[dbo].[spGetCashUnpaidBills] #dtmFromDate= ''' +#dtmFrom+ ''' ,#dtmToDate= ''' +#dtmTo+''',#numCompanyID=1,#numAdmDscTransID='+ #numAdmDscTransID +',#tnyShowIPCashSchemeBills=1',
#sql nvarchar(max),
#tableName Varchar(60) = 'Temp'
set #sql = 'create table ##' + #tableName + ' ('
begin
select #sql = #sql + '[' + r.name + '] ' + r.system_type_name + ','
from sys.procedures AS p
cross apply sys.dm_exec_describe_first_result_set_for_object(p.object_id, 0) AS r
where p.name = #procname
set #sql = substring(#sql,1,len(#sql)-1) + ')'
execute (#sql)
execute('insert ##' + #tableName + ' exec ' + #procWithParam)
end
SELECT *FROM ##Temp
If the both result sets have same number of columns then
insert into #T1 exec dbo.pVendorBalance
will insert the union of both data set into #T1.
If not
Then edit dbo.pVendorBalance and insert results into temporary tables and in outer stored proc, select from those temporary tables.
Another way(If you need it), you can try
SELECT * into #temp
from OPENROWSET('SQLNCLI', 'Server=(local)\\(instance);Trusted_Connection=yes;',
'EXEC dbo.pVendorBalance')
it will take first dataset.

Resources