Using sp_executesql with input and output paramters? - sql

Using SQL Server 2016. I am writing a stored procedure. I have the following code that is happening inside a WHILE loop
DELCARE #recordExists int;
SET #recordExistsQuery = 'SELECT #recordExists=COUNT(*) FROM #fullTableName WHERE validFrom <= CAST(#asOfDate as datetime)';
exec sp_executesql #recordExistsQuery, N'#recordExists INT OUT, #fullTableName varchar(60), #asOfdate datetime' #recordExists OUT
I get the error `Must declare the table variable #fullTableName however I have already declared and set this variable (prior to the while loop) and used it multiple times prior the while loop so I know it exists and is valid. It's defined like so -
DECLARE #fullTableName varchar(60);
SET #fullTableName = (SELECT CONCAT(#schema, '.', #TableName));
and I have it printed prior to the while loop and it looks fine, and I have it printer per loop and that works as well.
What is wrong with my dynamic sql here? I am trying to use the paramterization method instead of string building with quotes as I am dealing with a datetime and want to do that with better practices than a bunch of quotes. Is that possible? How can I rewrite
SET #recordExistsQuery = 'SELECT #recordExists=COUNT(*) FROM #fullTableName WHERE validFrom <= CAST(#asOfDate as datetime)';
exec sp_executesql #recordExistsQuery, N'#recordExists INT OUT, #fullTableName varchar(60), #asOfdate datetime' #recordExists OUT
so that it works as expected?
UPDATE: Hardcoding the tablename instead of passing it as a parameter worked. I am now getting the following error from the following code -
SET #recordExistsQuery = 'SELECT #recordExists=COUNT(*) FROM ' + #fullTableName + ' WHERE validFrom<=CAST(#asOfDate as datetime)';
PRINT #recordExistsQuery
exec sp_executesql #recordExistsQuery, N'#asOfDate datetime, #recordExists INT OUT', #recordExistsOut`
The print statement shows
SELECT #recordExists=COUNT(*) FROM [MySchema].[MyTable] WHERE validFrom<=(#asOfDate as datetime)
The error I now get is
Msg 8162, Level 16, State 2, Line 0
The formal parameter "#asOfDate" was not declared as an OUTPUT paramter, but the actual paramter passed in requested output.
I have #asOfDate as a paramter of my stored proecedure defined as
#asOfDate DATETIME=NULL and the first line of my sp sets a default value if none is passed in
IF #asOfdate IS NULL
SET #asOfDate = GETDATE();
Anyone know what is going wrong now?
UPDATE 2:
Using this line instead
'SELECT #recordExists=COUNT(*) FROM '+ #fullTableName +' WHERE validFrom <= CAST('+#asOfDate'+' as datetime)'
Now gets me the error Msg 241, Level 16, State 1, Conversion failed when converting date and/or time from character string.
#asOfDate is defined as a paramter of my sp like #asOfDate datetime=NULL and initialized to getdate(). I am testing this with #asOfDate=null, so it is using getdate() to initialize the value.

Try splitting the string like this
'SELECT COUNT(*) FROM'+ #fullTableName +'WHERE validFrom <= CAST('+#asOfDate+' as datetime)'
since #fullTableName is going as a string and not a variable, it was not working. If you change to the above, the final string will have the table name you wanted
This update should fix the asofdate issue
Also, you need to remove #recordExists and use the response from execution as #recordExists

Could not quite get it to work as it was, so I ended up doing the following - when I initialized the date I also made a formatted version of it -
paramters -
#asOfDate DATETIME=NULL,
...
BEGIN
IF #asOfdate IS NULL
SET #asOfDate = GETDATE()
DECLARE #asOfDateFormatted NVARCHAR(30);
SET #asOfDateFormatted = CONVERT(NVARCHAR(30), #asOfDate, 126);
...
Dynamic sql -
SET #recordExistsQuery = 'SELECT COUNT(*) FROM '+ #fullTableName +' WHERE validFrom <= '''+asOfDateFormatted+'''';
exec sp_executesql #recordExistsQuery, N'#recordExists INT OUT', #recordExists OUT
If someone knows a better way please let me know but this is how I got it to work for now.

When using dynamic SQL, bear in mind the following:
Table names (and other objects such as columns) cannot be passed as parameters, they must be injected.
Only do this using QUOTENAME to ensure proper escaping
Object names should be stored in sysname (alias for nvarchar(128)) and queries in nvarchar(max)
Data should always be passed as parameters, not injected
DECLARE #asOfDate DATETIME='20210923',
IF #asOfdate IS NULL
SET #asOfDate = GETDATE();
DECLARE #recordExistsQuery nvarchar(max) = N'
SELECT #recordExists = COUNT(*)
FROM ' + QUOTENAME(#schema) + '.' + QUOTENAME(#TableName) + '
WHERE validFrom <= #asOfdate;
';
PRINT #recordExistsQuery; -- for testing
EXEC sp_executesql
#recordExistsQuery,
N'#recordExists INT OUT, #asOfdate DATETIME',
#recordExists = #recordExists OUT,
#asOfdate = #asOfdate;

Related

Problem with a dynamic function in SQL Server

I have a table dbo.t_products and I want to know the last record updated. For that I have an attribute last_changed which stores for each record the timestamp of the last update.
Finally I want to save that result in a variable called #y.
DECLARE #y DATETIME
DECLARE #p_table VARCHAR(100)
SET #p_table = 'dbo.t_products'
EXECUTE sp_executesql N'SET #y = SELECT MAX(last_changed) FROM #p_table'
,N'#p_table VARCHAR, #y DATETIME OUTPUT'
,#p_table
,#y OUTPUT
SELECT #y
The system returns the following message:
Msg 156, Level 15, State 1, Line 25
Incorrect syntax near the keyword 'SELECT'.
Any ideas?
The whole point of using dynamic SQL in your case (I assume) is to allow the use of a dynamic table name. In which case you have to insert the table name into the dynamic SQL string - you can't pass it in as a parameter, thats the problem you are trying in the first place.
Also you don't need a SET followed by a SELECT just use SELECT directly to set the variable.
Finally you definitely want to use the QUOTENAME function to escape your table name and avoid an SQL injection attack - this requires you split the table name and schema name.
DECLARE #y DATETIME;
DECLARE #p_schema VARCHAR(100);
DECLARE #p_table VARCHAR(100);
DECLARE #SQL NVARCHAR(max);
SET #p_schema = 'dbo';
SET #p_table = 't_products';
-- Add the table name to the dynamic SQL
SET #SQL = 'SELECT #y = MAX(last_changed) FROM ' + QUOTENAME(#p_schema) + '.' + QUOTENAME(#p_table);
EXECUTE sp_executesql #SQL, N'#y DATETIME OUTPUT', #y OUTPUT;
-- PRINT(#SQL); --- Debugging
SELECT #y;

Use Open Query with quoted items

I am trying to use openquery to access a linked server. However, it appears to be impossible because, you cannot have the correct number of Single quotes. I need to pass a variable Start and End date, so I cannot use the basic openquery method, but instead must use the EXEC(#OPENQUERY+ #SQL) method. The problem is, to pass the date through the #SQL variable I must use ''' so it has 1 quote, but then when it gets passed to the EXEC(OPENQUERY+#SQL) the open query which introduces another level of quotes, causes the dates to now be not be quoted and I get an error. If I add another layer of quotes it then causes them to be double quotes causing that error. Is it not possible to use quotes in an open query? I have the same issue even passing things like Where Username = 'Jack'. I can never have the correct number of quotes.
DECLARE #STARTDT NVARCHAR(10) = '2019-01-01'
,#ENDDT NVARCHAR(10) = '2019-03-01'
DECLARE #SQL NVARCHAR(4000)
DECLARE #OPENQUERY nvarchar(4000)
, #LinkedServer nvarchar(4000)
SET #LinkedServer = 'ProductionSvr'
SET #SQL =
'select *
from SalesData a
where a.Sale_date between ''' + #StartDt + ''' and ''' + #ENDDT + ''')
'''
print #SQL
SET #OPENQUERY = 'SELECT * FROM OPENQUERY('+ #LinkedServer + ','''
EXEC (#OPENQUERY+#SQL)
Within a quoted string, you have to escape a single quote with an additional single quote, which is where you start to see the four and five single quotes in a row. In order to help simplify things, I would make a couple of suggestions.
First, keep your strings more segregated in their duties. Your #SQL variable includes the necessary ending punctuation for your OPENQUERY, but the beginning punctuation is in the #OPENQUERY variable. (This might be clearer in the code below.)
Also, I'd recommend using proper data types (such as your dates), and then use the CONCAT function, which conveniently handles all of the data type conversions for you.
So, the modified version of what you started with would look like this:
DECLARE #OPENQUERY nvarchar(4000)
, #LinkedServer nvarchar(256)
, #SQL NVARCHAR(4000);
--Set your dates. Use the right data type to avoid sending
--invalid dates into your query. Easier to debug them here.
DECLARE #STARTDT DATE = '2019-01-01'
,#ENDDT DATE = '2019-03-01';
--Set your server.
SET #LinkedServer = 'ProductionSvr';
--Then set up the inner query.
SET #SQL =
CONCAT(
'select *
from SalesData a
where a.Sale_date between ''', #StartDt, ''' and ''', #ENDDT,'''');
--Set up the OPENQUERY variable with all of the punctuation that it needs,
--so you just need to drop in your LinkedServer name and your SQL statment.
--Use CONCAT because it handles the data type conversions for you.
SET #OPENQUERY = CONCAT('SELECT * FROM OPENQUERY(',#LinkedServer,',''(',#SQL,')'')');
PRINT #OPENQUERY;
Result:
SELECT * FROM OPENQUERY(ProductionSvr,'(select *
from SalesData a
where a.Sale_date between '2019-01-01' and '2019-03-01')')
How about this:
DECLARE #STARTDT NVARCHAR(10) = '2019-01-01'
,#ENDDT NVARCHAR(10) = '2019-03-01'
DECLARE #SQL NVARCHAR(4000)
DECLARE #OPENQUERY nvarchar(4000)
, #LinkedServer nvarchar(4000)
SET #LinkedServer = 'ProductionSvr'
SET #SQL =
'SELECT * FROM OPENQUERY(' + #LinkedServer + ',''select *
from SalesData a
where a.Sale_date between ''' + #StartDt + ''' and ''' + #ENDDT + ''')'
print #SQL
All you have to remember is within quotes, double single quotes means 1 single quote. So, ''')' essentially translates to "')". Hope this helps.

Select into tables dynamically with variables

I have some code to create tables based on a set of dates I define.
Example, I have 5 dates, and they are aren't consecutive. For any of these dates, I want to create a table and I am currently using a Select into.
I am having to do this 5 times, even though the only thing changing is the name of the new table created and the date. Is there a way to do this in an elegant way.
I started writing some code, but I am struggling to get it to loop through all the dates I want. The way I have written it currently, I only works if I edit the date at the start.
DECLARE #MyDate DATE;
SET #MyDate = '2019-01-01';
SET #TableName = 'Table1';
SELECT *
into #TableName
FROM Original_Table
WHERE Query_Date = #MyDate;
Is this a one time thing or do you have to do this on a regular basis?
If it's the first, than I would just do it and get it over with.
If it's the latter, then I suspect something is very wrong with the way that system is designed - but assuming that can't be changed, you can create a stored procedure that will do this using dynamic SQL.
Something like this can get you started:
CREATE PROCEDURE dbo.CreateTableBasedOnDate
(
#MyDate DATE,
-- sysname is a system data type for identifiers: a non-nullable nvarchar(128).
#TableName sysname
)
AS
-- 200 is long enough. Yes, I did the math.
DECLARE #Sql nvarchar(200) =
-- Note: I'm not convinced that quotename is enough to protect you from sql injection.
-- you should be very careful with what user is allowed to execute this procedure.
N'SELECT * into '+ QUOTENAME(#TableName) +N'
FROM Original_Table
WHERE Query_Date = #MyDate;';
-- When dealing with dynamic SQL, Print is your best friend.
-- Remark this row and unremark the next only once you've verified you get the correct SQL
PRINT #SQL;
--EXEC sp_ExecuteSql #Sql, N'#MyDate Date', #MyDate
GO
Usage:
EXEC CreateTableBasedOnDate '2018-01-01', 'zohar';
Use dynamic SQL:
DECLARE #MyDate DATE, #TableName varchar(50);
SET #MyDate = '2019-01-01';
SET #TableName = 'Table1';
DECLARE #sql NVARCHAR(4000);
DECLARE #params NVARCHAR(4000);
SELECT #sql=N'
SELECT *
INTO ' + QUOTENAME(#TableName) + '
FROM Original_Table
WHERE Query_Date = #MyDate;';
SELECT #params = N'#MyDate DATE';
EXEC sys.sp_executesql #sql, #params, #MyDate=#MyDate
Note that dynamic SQL can be dangerous as it opens up a path for SQL injection. Its fine if you are just using it in your own local scripts, but take care if you e.g. wrap this in a procedure that is more widely accessible.
I would use dynamic SQL although I would add another variables for the schema:
DECLARE
#MyDate nVarchar(50) = '2019-01-01',
#Schema nVarchar (50) = 'dbo',
#TableName nVarchar(250) = 'Table1',
#SQL nVarchar(500);
Set #SQL = '
SELECT *
into '+ QUOTENAME(#Schema)+'.'+ QUOTENAME(#TableName) +'
FROM Original_Table
WHERE Query_Date = '+ #MyDate +';
'
--print #SQL
Exec(#SQL)
You can use the print statement to see how the SQL will look before executing this properly. You may also want to look at adding this as a stored procedure.

Removing NULL with COALESCE in Dynamic SQL

I have some dynamic SQL as part of a stored procedure I want to execute:
SET #SQL_TXT = 'INSERT INTO ' +#ENTY_TABLE_NAME+
'([ITEM_NAME]
,[ADD_DTT]
,[ADD_USR]
,[UPD_DTT]
,[UPD_USR]
,[ACTIVE_IND]
,[ITEM_PK])
VALUES
('''+#UPD_VALUE+'''
, CURRENT_TIMESTAMP
, '''+#UPD_USR_DOM_NAME+''', CURRENT_TIMESTAMP,'''+#UPD_USR_DOM_NAME+''',''Y'','''+#ITEM_PK+''');
SET #Id = SCOPE_IDENTITY();
RETURN;'
This runs fine, but ITEM_NAME can't be NULL so I want to ad a COALESE():
SET #SQL_TXT = 'INSERT INTO ' +#ENTY_TABLE_NAME+
'(COALESCE([ITEM_NAME], '')
,[ADD_DTT]
,[ADD_USR]
,[UPD_DTT]
,[UPD_USR]
,[ACTIVE_IND]
,[ITEM_PK])
VALUES
('''+#UPD_VALUE+'''
, CURRENT_TIMESTAMP
, '''+#UPD_USR_DOM_NAME+''', CURRENT_TIMESTAMP,'''+#UPD_USR_DOM_NAME+''',''Y'','''+#ITEM_PK+''');
SET #Id = SCOPE_IDENTITY();
RETURN;'
But I am getting this error:
Incorrect syntax near the keyword 'COALESCE'.
Unclosed quotation mark after the character string ');
SET #Id = SCOPE_IDENTITY();
RETURN;'.
Incorrect syntax near '='.
For the life of me I don't see where this ')' is. What am I doing wrong?
Edit: here is the exec
EXECUTE SP_executesql #SQL_TXT, N'#Id INTEGER OUTPUT', #Id OUTPUT
You can not put coalesce() around the column name destination of your insert, you use it around the value being inserted.
SET #SQL_TXT = 'INSERT INTO ' +#ENTY_TABLE_NAME+
'([ITEM_NAME]
,[ADD_DTT]
,[ADD_USR]
,[UPD_DTT]
,[UPD_USR]
,[ACTIVE_IND]
,[ITEM_PK])
VALUES
('''+coalesce(#UPD_VALUE,'')+'''
, CURRENT_TIMESTAMP
, '''+#UPD_USR_DOM_NAME+''', CURRENT_TIMESTAMP,'''+#UPD_USR_DOM_NAME+''',''Y'','''+#ITEM_PK+''');
SET #Id = SCOPE_IDENTITY();
RETURN;'
Note: #UPD_USR_DOM_NAME is inserted into two different columns. Not sure if that is intentional, just thought I would point it out.
You can also fully parameterize the rest of your values for use with sp_executesql instead of concatenating them like that. (Guessing at the data types of your parameters in this example)
declare #sql nvarchar(max);
declare #params nvarchar(max);
declare #id int;
set #sql = N'INSERT INTO ' +#ENTY_TABLE_NAME+'([ITEM_NAME] ,[ADD_DTT] ,[ADD_USR] ,[UPD_DTT] ,[UPD_USR] ,[ACTIVE_IND] ,[ITEM_PK])
VALUES (coalesce(#UPD_VALUE,''), CURRENT_TIMESTAMP, #UPD_USR_DOM_NAME, CURRENT_TIMESTAMP,#UPD_USR_DOM_NAME,''Y'',#ITEM_PK);
SET #Id = SCOPE_IDENTITY();
RETURN;'
set #params = N'#UPD_VALUE varchar(32),#UPD_USR_DOM_NAME varchar(32), #ITEM_PK varchar(32), #Id INTEGER OUTPUT';
EXECUTE SP_executesql #sql, #params, #UPD_Value, #UPD_USER_DOM_NAME, #ITEM_PK, #Id = #Id OUTPUT;
dynamic sql reference:
The curse and blessings of dynamic SQL - Erland Sommarskog
sp_executesql

Difficulty using a CHAR in Dynamic SQL

I'm trying to pass in #accountType, a char value to a stored procedure that uses dynamic SQL. It is declared as char(4) in the procedure. The current error is Incorrect syntax near 'D' if I try to change it I get invalid column: D.
I cannot figure out how dynamic SQL wants me to indicate that the variable is a char. I've tried it many ways, here is the most recent:
set #q = 'Update ' + #statementTable +
' SET Account = '+ #padding + #accountNumber +
' WHERE ClosingDate BETWEEN CAST('''+CONVERT(VARCHAR(20),#proc_dateStart)+''' AS DATE) AND CAST('''+CONVERT(VARCHAR(20),#proc_dateEnd)+''' AS DATE)' +
' AND AccountType =' + ''''+ #accountType +''''
The value is coming from my C# code exactly like this: D
No single quotes or anything around the letter. Any ideas? I'm more than a bit stuck with this.
Something like this, you need to have the parameters actually within the string statement, then when you execute sp_executesql, you then pass what each of those parameters are.
DECLARE #q VARCHAR(MAX)
DECLARE #statementTable VARCHAR(50)
DECLARE #padding VARCHAR(50)
DECLARE #accountNumber CHAR(4)
DECLARE #proc_dateStart VARCHAR(50)
DECLARE #proc_dateEnd VARCHAR(50)
DECLARE #accountType VARCHAR(50)
SET #q = 'Update #statementTable
SET Account = ''#accountNumber''
WHERE ClosingDate BETWEEN CAST(''+CONVERT(VARCHAR(20),#proc_dateStart)+'' AS DATE) AND CAST(''+CONVERT(VARCHAR(20),#proc_dateEnd)+'' AS DATE)
AND AccountType = ''#accountType'''
EXEC sys.sp_executesql #sql, N'#statementTable VARCHAR(50),#accountNumber CHAR(4),#proc_dateStart VARCHAR(50), #proc_dateEnd VARCHAR(50),#accountType VARCHAR(50)',
#statementTable,#accountNumber,#proc_dateStart,#proc_dateEnd,#accountType;