In a stored procedure I am dynamically creating a query with a INSERT. This is done in order to force default values (like with #name if it is NULL).
SET #sql = 'INSERT INTO table (username, password'
+ CASE #name IS NULL THEN '' ELSE ',name' END
+ ') VALUES (''root'',''gelehallon''' +
+ CASE #name IS NULL THEN '' ELSE ',''#name''' END
+ ')'
EXEC sp_executesql #sql
SET #id = SCOPE_IDENTITY()
#id will be 0 no matter.
How can I retrieve the IDENTITY in a safe manner even if another thread is running the same stored procedure simultaneously?
SET #sql = 'INSERT INTO table (username, password) VALUES (#username,#pwd)
SELECT #id = SCOPE_IDENTITY()'
EXEC sp_executesql #sql,
N'#username VARCHAR(50), #pwd VARCHAR(50), #id INTEGER OUTPUT',
'root', 'gelehallon', #id OUTPUT
-- #id now has SCOPE_IDENTITY() value in
Though a few points:
- assuming this is a simplified example as there doesn't seem to be a need to use dynamic SQL in this example
- assuming you're not going to store real passwords in plain text in the db!
Alternatively, you can use the OUTPUT clause with the INSERT statement. That will cause the dynamic statement, and, consequently, the system stored procedure used to invoke it, to return a rowset (one row in your case). You can grab at the chance and insert the rowset into a table variable, and then read the value.
Basically, it might look like this:
SET #sql = 'INSERT INTO table (...) OUTPUT inserted.ID VALUES (...)';
DECLARE #ScopeIdentity (ID int);
INSERT INTO #ScopeIdentity
EXEC sp_executesql #sql;
SELECT #id = ID FROM #ScopeIdentity;
Related
I have created a stored procedure as shown below, but it's returning only one row instead of 3:
CREATE PROCEDURE [dbo].[tempsp]
(#RecycleIds NVARCHAR(MAX) = NULL)
AS
BEGIN
DECLARE #Err INT
DECLARE #WhereClause NVARCHAR(MAX)
DECLARE #SQLText1 NVARCHAR(MAX)
DECLARE #SQLText NVARCHAR(MAX)
SET #SQLText1 = 'SELECT FROM dbo.SKU '
IF #RecycledSkuIds IS NOT NULL
BEGIN
SET #SQLText = 'SELECT FROM dbo.SKU WHERE SKU.SkuId IN (#RecycleIds)'
EXEC sp_executesql #SQLText, N'#RecycleSkuIds nvarchar', #RecycleIds
END
ELSE
BEGIN
EXEC(#SQLText1)
END
SET #Err = ##ERROR
RETURN #Err
END
-------end of stored procedure--------
EXEC tempsp #RecycleIds = '5,6,7'
After running this SQL statement, it only returns one row instead of 3, with the id's of 5, 6, 7.
Can anyone tell me what I am doing wrong?
i wanted to use sp_executesql, so that it can be safe against sql injection with strong type defined.
Use a table type parameter, with a strongly typed column:
CREATE TYPE dbo.IDs AS table (ID int);
GO
CREATE PROCEDURE [dbo].[tempsp] #RecycleIds dbo.IDs READONLY AS
BEGIN
IF EXISTS (SELECT 1 FROM #RecycleIds)
SELECT * --Replace with needed columns
FROM dbo.SKU S
--Using EXISTS in case someone silly puts in the same ID twice.
WHERE EXISTS (SELECT 1
FROM #RecycleIds R
WHERE R.ID = S.SkuID);
ELSE
SELECT * --Replace with needed columns
FROM dbo.SKU S
END;
GO
Then you could execute it like so:
EXEC dbo.tempsp; --All Rows
GO
DECLARE #RecycleIds dbo.IDs;
INSERT INTO #RecycleIds
VALUES(1),(40),(182);
EXEC dbo.tempsp #RecycleIds;
I was trying to retrive the rows whose id matches within the IN clause.
SET #INClauseIds='''' + replace(#Ids, ',', ''',''') + ''''
Above statement would convert the ID's ='1,2,3' to '1','2','3' which i can directly place in the IN clause.
SET #SQLText1 ='EXEC(''SELECT Name,SEOFriendlyName FROM SKU Where Id IN ( ''+ #Ids+'' ) )'
EXEC sp_executesql #SQLText1 ,N'#INClauseIds nvarchar(max)',#Ids=#INClauseIds
If you want to avoid the usage of Temp Table which would add extra caliculation time. you can you the above strategy to retrive n number of records. Safe with strongly coupled with sp_executesql and without any sql injection.
You cannot use IN. Or, more accurately, you have a string and you are confusing it with a list. One method is to instead use LIKE:
SET #SQLText = '
SELECT *
FROM dbo.SKU
WHERE CONCAT('','', #RecycleIds, '','') LIKE CONCAT(''%,'', SKU.SkuId, '',%'')
';
The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns. This is my code for the stored procedure:
ALTER proc [dbo].[usp_ColumnFieldValidator]
(
#TblName nvarchar(30),
#ColumnName nvarchar(30),
#RetVal bit output
)
as
begin
declare #CountOfRowsQuery as nvarchar(300)
set #CountOfRowsQuery = 'select count('+quotename(#ColumnName)+') from '+quotename(#TblName)+' having count(' +quotename(#ColumnName)+') = nullif(count('+quotename(#ColumnName)+'),0)'
execute sp_executesql #CountOfRowsQuery
select #RetVal = dbo.fn_ColumnValidator(#CountOfRowsQuery)
end
As you can see, a user-defined function is being called to set the value of #RetVal. This is my code for the user-defined function.
ALTER function [dbo].[fn_ColumnValidator]
(
#NullChecker as nvarchar(max)
)
returns bit
as
begin
declare #returnVar as bit
if #NullChecker is null
set #returnVar = 0
else
set #returnVar = 1
return #returnVar
end
The output of #RetVal is always 1 and I have attributed this error to #CountOfRowsQuery storing the entire string rather than the value of the query ie: #CountOfRowsQuery = null if the count of rows is zero else, #CountOfRowsQuery = the number of rows present in the column. To make things clearer I am attaching screenshots of the output when I run the program.
Output of a table that contains rows with data
Output of a table that contains no rows with no data
As you can see in list item.2, the sp returns null but the function_returned_value is being set to 1 instead of 0.
The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns.
Man, if this is not an over-complication I don't know what is.
Here's a much simpler (and more efficient) query that does the work:
SELECT CAST(IIF(EXISTS(
SELECT 1
FROM TableName
WHERE ColumnName IS NOT NULL
), 1, 0) As Bit)
Now, to change that to a procedure using dynamic SQL in a way that will not expose you to SQL Injection threats you can do this:
ALTER PROCEDURE [dbo].[usp_ColumnFieldValidator]
(
#TblName sysname,
#ColumnName sysname,
#RetVal bit output
)
AS
BEGIN
IF NOT EXISTS(
SELECT 1
FROM Information_Schema.Columns
WHERE Table_Name = #TblName
AND Column_Name = #ColumnName
)
RETURN;
DECLARE #Sql nvarchar(1000) =
N'SELECT #RetVal = CAST(IIF(EXISTS(
SELECT 1
FROM '+ QUOTENAME(#TblName) + N'
WHERE '+ QUOTENAME(#ColumnName) + N' IS NOT NULL
), 1, 0) As Bit)'
EXEC sp_executesql #Sql, N'#RetVal bit output', #RetVal OUTPUT;
END
Key notes:
I've changed the #TblName and #ColumnName variables to data type sysname instead of your original nvarchar(30) - since that is the data type SQL Server use internally to store identifiers.
Since identifiers can't be parameterized, I've white-listed them.
I'm using sp_executeSql to get back the value of the dynamic query directly into my output parameter.
For more tips and tricks on dynamic SQL, you can read my blog post entitled The do’s and don’ts of dynamic SQL for SQL Server
Please see the DDL below:
CREATE TABLE TestTable (id int identity, name varchar(30))
INSERT INTO TestTable (Name) VALUES ('Ian')
declare #Test As varchar(100)
set #Test = 'Name'
SELECT #Test FROM TestTable
The output from the SELECT is 'Name'. I want the output to be: 'Ian'. How?
You can't use a variable to tell SQL Server what column, table, database etc. you want to use. You need to enclose this type of code in dynamic SQL.
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT ' + QUOTENAME(#Test) + ' FROM dbo.TestTable;';
EXEC sp_executesql #sql;
Here's why I prefer sp_executesql instead of EXEC() as a standard best practice, and here's why you should always use the schema prefix (e.g. dbo.) when referencing objects. QUOTENAME() can help protect you from SQL injection in this case, since I don't know where the value for #Test ultimately comes from.
You can use EXEC to execute a fabricated SQL string:
declare #Test As varchar(100)
set #Test = 'Name'
EXEC ('SELECT ' + #Test + ' FROM TestTable')
The standard warning for these answers is be certain that you can control what gets put in the SQL statement or use restrictive rights (e.g. read-only) to execute them, otherwise you could get something like this:
declare #Test As varchar(100)
set #Test = '1 ; DROP TABLE TestTable; --'
EXEC ('SELECT ' + #Test + ' FROM TestTable')
Generate a string dynamically and use exec
EXEC ('SELECT ' + #Test + ' FROM TestTable')
Not sure how to implement this, but I need a way to get the current list of parameters for a stored procedure as well as their passed in values (this code will be executed in the stored procedure itself).
I know I can use sys.parameters to get the parameter names, but how to get the actual values?
What I need to do with this is to make a char string of the form
#param_name1=#value1,#param_name2=#value2,...,#param_namen=#valuen
I have tried to use dynamic sql, but not having much joy with that.
Any ideas??
Edit:
Currently I am just going through all the parameters one-by-one to build the string. However I want a "better" way to do it, since there are quite a few parameters. And incase parameters are added later on (but the code to generate the string is not updated).
I tried using dynamic sql but gave up, since the sp_executesql sp requires parameters be passed into it...
You state '(this code will be executed in the stored procedure itself).' so assuming you are in the procedure you will already know the parameter names as you have to declare them when creating your procedure. Just do a select and put the names inside text fields
ALTER PROCEDURE procname
(
#param1 NVARCHAR(255)
,#param2 INT
...
)
SELECT [Parameters] = '#param1=' + #param1
+ ',#param2=' + CONVERT(NVARCHAR(MAX),#param2)...
The CONVERT is there as an example for non-char datatypes.
update
You will need to create a linked server that points to itself to use the OPENQUERY function.
USE [master]
GO
/****** Object: LinkedServer [.] Script Date: 04/03/2013 16:22:13 ******/
EXEC master.dbo.sp_addlinkedserver #server = N'.', #srvproduct=N'', #provider=N'SQLNCLI', #datasrc=N'.', #provstr=N'Integrated Security=SSPI'
/* For security reasons the linked server remote logins password is changed with ######## */
EXEC master.dbo.sp_addlinkedsrvlogin #rmtsrvname=N'.',#useself=N'True',#locallogin=NULL,#rmtuser=NULL,#rmtpassword=NULL
GO
Now you can do something like this cursor to get each parameter name and then use dynamic sql in OPENQUERY to get the value:
DECLARE curParms CURSOR FOR
SELECT
name
FROM sys.parameters
WHERE OBJECT_ID = OBJECT_ID('schema.procedurename')
ORDER BY parameter_id
OPEN curParms
FETCH curParms INTO #parmName
WHILE ##FETCH_STATUS <> -1
BEGIN
SELECT #parmName + '=' + (SELECT * FROM OPENQUERY('linkedservername','SELECT ' + #parmName))
FETCH curParms INTO #parmName
END
CLOSE curParms
DEALLOCATE curParms
Since SQL Server 2014 we have sys.dm_exec_input_buffer, it is a table valued function with an output column event_info that gives the full execution statement (including parameters).
We can parse the param values from sys.dm_exec_input_buffer and get the param names from sys.parameters and join them together to get the string you want.
For example:
create procedure [dbo].[get_proc_params_demo]
(
#number1 int,
#string1 varchar(50),
#calendar datetime,
#number2 int,
#string2 nvarchar(max)
)
as
begin
-- get the full execution statement
declare #statement nvarchar(max)
select #statement = event_info
from sys.dm_exec_input_buffer(##spid, current_request_id())
-- parse param values from the statement
declare #proc_name varchar(128) = object_name(##procid)
declare #param_idx int = charindex(#proc_name, #statement) + len(#proc_name)
declare #param_len int = len(#statement) - #param_idx
declare #params nvarchar(max) = right(#statement, #param_len)
-- create param values table
select value, row_number() over (order by current_timestamp) seq
into #params
from string_split(#params, ',')
-- get final string
declare #final nvarchar(max)
select #final = isnull(#final + ',','') + p1.name + '=' + ltrim(p2.value)
from sys.parameters p1
left join #params p2 on p2.seq = parameter_id
where object_id = ##procid
select #final params
end
To test it:
exec get_proc_params_demo 42, 'is the answer', '2019-06-19', 123456789, 'another string'
Returns the string you want:
#number1=42,#string1='is the answer',#calendar='2019-06-19',#number2=123456789,#string2='another string'
I have something similar wrapped as a UDF. I use it for error logging in catch blocks.
I am using execute sp_executesql to execute a insert query in sp. Query executed, insert happen properly. After that I am using scope indentity(), value is set into one output para. But I am getting error:
procedure attempted to return a status of NULL, which is not allowed. A status of 0 will be returned instead.
How to solve it? Does anybody having an idea?
The SCOPE_IDENTITY() function returns the last identity value inserted in the current scope. The dynamic query you are executing with EXEC sp_executesql is not in the current scope. You need to call SCOPE_IDENTITY() in your dynamic query or use a different method to achieve the result you want.
Here's how you could use SCOPE_IDENTITY():
DECLARE #ID int;
EXEC sp_executesql N'
INSERT INTO …;
SET #ID = SCOPE_IDENTITY();
', N'#ID int OUTPUT', #ID OUTPUT;
RETURN #ID;
Or you could try the following:
DECLARE #LastID TABLE (ID int);
INSERT INTO #LastID (ID)
EXEC sp_executesql N'
INSERT INTO …;
SELECT SCOPE_IDENTITY();
';
RETURN (SELECT TOP 1 ID FROM #LastID);
If you are on SQL Server 2005+, you could modify the above method to use the OUTPUT clause of INSERT instead of SELECT SCOPE_IDENTITY().