I have a SQL script like below. I am using QUOTENAME to construct the name of the table to query. It is giving me an Invalid object name old.tb2018 error because QUOTENAME is generating the table name with brackets like so - [old.tb2018].
I have considered passing tablename as a parameter to sp_executesql, but I am using #old_table_name in other places in the code. I fear that will create a bunch of duplicate code because of all the other places that is being used in. How do I pass the tablename to #trunc_success without it generating an error?
DECLARE #YEAR NVARCHAR(4) = year(DATEADD(YEAR, - 1, getdate()))
DECLARE #old_table_name NVARCHAR(500)
DECLARE #new_table_name NVARCHAR(500)
DECLARE #trunc_success NVARCHAR (3000)
SET #old_table_name = QUOTENAME('old.tb' + #year);
SET #new_table_name = QUOTENAME('new.tb' + #year);
SET #trunc_success = '
SELECT
CASE
WHEN EXISTS
(
--This should no records
SELECT
top 1
*
FROM ' + #old_table_name + '
) THEN ''Failed''
ELSE ''Passed''
END result
UNION ALL
SELECT
CASE
WHEN EXISTS
(
--This should return no records
SELECT top 1 *
FROM ' + #new_table_name + '
) THEN ''Failed''
ELSE ''Passed''
END'
print #trunc_success
EXECUTE sp_executesql #trunc_success
Your table's name isn't old.tb2018 it's tb2018 and it's on the schema old. It should be:
DECLARE #old_table_name sysname; --Correct data type for object names
DECLARE #new_table_name sysname; --Correct data type for object names
...
SET #old_table_name = N'old.' + QUOTENAME(N'tb' + #year);
SET #new_table_name = N'new.' + QUOTENAME(N'tb' + #year);
...
If you want to quote the schema name too (good when not using a literal value) it would be:
QUOTENAME(N'new') + N'.' + QUOTENAME(N'tb' + #year);
Although I've used sysname here, on second though, the idea should be different. It should really be
DECLARE #old_table_name sysname; --Correct data type for object names
DECLARE #new_table_name sysname; --Correct data type for object names
...
SET #old_table_name = QUOTENAME(N'tb' + #year);
SET #new_table_name = QUOTENAME(N'tb' + #year);
...
And then in your query you'd have:
'...FROM old.' + QUOTENAME(#old_table_name) + '...'
The reason for this is that sysname is a synonym for nvarchar(128) NOT NULL, however, QUOTENAME returns an nvarchar(258); therefore quoting 2 objects, schema and table with a delimitor (.), could potentially result in an nvarchar(517) (258 + 1 + 258). It's therefore better to split the individual parts into their own parameters/variables and use QUOTENAME at the point of injection. This will avoid any unexpected truncation if you do have any overly long object names, or have (foolishly) used lots of ] characters in it's name.
Of course, the only way you could get get a 258 character value for a real object's name from QUOTENAME would be if the object's name was made up of 128 ] characters ([ for the start and ] for the end, and 128 ] characters escaped to ]]):
SELECT LEN(QUOTENAME(REPLICATE(']',128)));
If you really did have an object named that, I would honestly be questioning your sanity.
Related
I'm trying to use the value from a variable being passed through from elsewhere in my where statement but i'm struggling on syntax.
I have a field that is being passed through where the value with in it is essentially a list
ie #Var1 where the value being passed through is AA','BB','CC (ie its missing the leading ' and trailing '
i want to use this value in my where statement as a list eg
where
field1 IN #Var1
ie - the actual result I want is
where
field1 IN ('AA','BB','CC')
I don't have any control over what is being passed through (ie AA','BB','CC will always be missing the leading and trailing ', so how do i use that info in the IN clause in the WHERE?
I seem to be coming up against issues with syntax around the use of the '
I hope that makes sense, its not the easiest thing to explain and its still relatively new to me
Here an example for my approach in the comments above:
CREATE PROCEDURE test_r(#x varchar(4000))
AS
BEGIN
DECLARE #t TABLE (keys NVARCHAR(10))
DECLARE #sql_xml XML = CAST('<root><X>'+REPLACE(#x, char(39)+','+char(39), '</X><X>') + '</X></root>' AS XML)
INSERT INTO #t(keys)
SELECT f.x.value('.', 'VARCHAR(10)') AS y
FROM #sql_xml.nodes('/root/X') f(x)
SELECT *
-- FROM [MyTable] m INNER JOIN...
FROM #t
END
GO
DECLARE #x AS nvarchar(4000) = 'AA' + char(39) + ',' + char(39) + 'BB' + char(39) + ',' + char(39) + 'CC'
EXEC test_r #x
Depending on what you have to do with the result of your query, you might want to create a function instead of a procedure.
My table has column names m1,m2,m3...,m12.
I'm using iterator to select them and insert them one by one in another table.
In this iterator I'm trying to generate filed names with:
'['+concat('m',cast(#P_MONTH as nvarchar))+']'
where #P_MONTH is incrementing in each loop.
so for #P_MONTH = 1 this suppose to give [m1] which works fine.
But when I run query I get:
Conversion failed when converting the nvarchar value '[m1]' to data
type int.
And if I put simply [m1] in that select it works ok.
How to concat filed name so it can be actually interpreted as filed name from certain table?
EDIT
Here is full query:
DECLARE #SQLString nvarchar(500),
#P_YEAR int,
#P_MONTH int = 1
set #P_YEAR = 2018
WHILE #P_MONTH < 13
BEGIN
SET #SQLString =
'INSERT INTO [dbo].[MASTER_TABLE]
(sector,serial,
date, number, source)'+
'SELECT ' + '[SECTOR],[DEPARTMENT]' +
QUOTENAME(cast(CONVERT(datetime,CONVERT(VARCHAR(4),#P_YEAR)+RIGHT('0'+CONVERT(VARCHAR(2),#P_MONTH),2)+'01',5) as nvarchar))+
QUOTENAME ('M',cast(#P_MONTH as nvarchar)) +
'EMPLOYED' +
'FROM [dbo].[STATS]'+
'where YEAR= #P_YEAR'
EXECUTE sp_executesql #SQLString
SET #P_MONTH = #P_MONTH + 1
END
It's still not working. It executes successfully but it does nothing.
Good day,
Let's create a simple table for the sake of the explanation
DROP TABLE IF EXISTS T
GO
CREATE TABLE T(a1 INT)
GO
INSERT T(a1) VALUES (1),(2)
GO
SELECT a1 FROM T
GO
When we are using a query like bellow, the server parse the text as a value and not as a column name
DECLARE #String NVARCHAR(10)
SELECT #String = '1'
--
SELECT '['+concat('a',cast(#String as nvarchar))+']'
FROM T
GO
This mean that the result will be 2 rows with no name for the column and the value will be "[a1]"
Moreover, the above query uses the brackets as part of the string.
One simple solution is to use the function QUOTENAME in order to add brackets around a name.
Another issue in this approach is the optional risk of SQL Injection. QUOTENAME might not be perfect solution but can help in this as well.
If we need to use entities name dynamically like in this case the column name then for most cases using dynamic query is the best solution. This mean to use the Stored Procedure sp_executesql as bellow
DECLARE #String INT
SELECT #String = 1
DECLARE #SQLString nvarchar(500);
SET #SQLString =
'SELECT ' + QUOTENAME(concat('a',cast(#String as nvarchar))) + ' FROM T'
EXECUTE sp_executesql #SQLString
GO
I want to create a stored procedure in SQL Server 2017 and let it be called somewhere else (i.e., Python). It accepts three parameters, stkname - stock name, stktype - stock type, colname - output columns (only these columns are returned). #colname is a varchar storing all required column names separated by ,.
If #colname is not specified, return all columns (*)
If stkname or stktype is not specified, do not filter by it
This is my code so far:
create procedure demo
(#stkname varchar(max),
#colname varchar(max),
#stktype varchar(max))
as
begin
----These lines are pseudo code -----
select
(if #colname is not null, #colname, else *)
from
Datatable
(if #stkname is not null, where stkname = #stkname)
(if #stktype is not null, where stktype = #stktype)
---- pseudo code end here-----
end
The desired result is that
exec demo #colname= 'ticker,price', #stktype = 'A'
returns two columns - ticker and price, for all records with stktype = 'A'
I could imagine 'dynamic SQL' would be possible, but not that 'elegant' and I need to write 2*2*2 = 8 cases.
How can I actually implement it in a better way?
PS: This problem is not a duplicate, since not only I need to pass column names, but also 'generate a query by other parameters'
You need dynamic SQL, but you don't need to write a case for every permutation, and it's really not all that hard to prevent malicious behavior.
CREATE PROCEDURE dbo.demo -- always use schema prefix
#stkname varchar(max) = NULL,
#colnames nvarchar(max) = NULL, -- always use Unicode + proper name
#stktype varchar(max) = NULL
AS
BEGIN
DECLARE #sql nvarchar(max);
SELECT #sql = N'SELECT '
+ STRING_AGG(QUOTENAME(LTRIM(RTRIM(x.value))), ',')
FROM STRING_SPLIT(#colnames, ',') AS x
WHERE EXISTS
(
SELECT 1 FROM sys.columns AS c
WHERE LTRIM(RTRIM(x.value)) = c.name
AND c.[object_id] = OBJECT_ID(N'dbo.DataTable')
);
SET #sql += N' FROM dbo.DataTable WHERE 1 = 1'
+ CASE WHEN #stkname IS NOT NULL THEN
N' AND stkname = #stkname' ELSE N'' END
+ CASE WHEN #stktype IS NOT NULL THEN
N' AND stktype = #stktype' ELSE N'' END;
EXEC sys.sp_executesql #sql,
N'#stkname varchar(max), #stktype varchar(max)',
#stkname, #stktype;
END
It's not clear if the stkname and stktype columns really are varchar(max), I kind of doubt it, you should replace those declarations with the actual data types that match the column definitions. In any case, with this approach the user can't pass nonsense into the column list unless they somehow had the ability to add a column to this table that matched their nonsense pattern (and why are they typing this anyway, instead of picking the columns from a defined list?). And any data they pass as a string to the other two parameters can't possibly be executed here. The thing you are afraid of is probably a result of sloppy code where you carelessly append user input and execute it unchecked, like this:
SET #sql = N'SELECT ' + #weapon_from_user + '...';
For more on malicious behavior see:
Protecting Yourself from SQL Injection in SQL Server - Part 1
Protecting Yourself from SQL Injection in SQL Server - Part 2
I'm quite new to SQL Server so hopefully this makes sense :)
I'm trying to declare variables to be used in an INNER JOIN.
If you take a look at my code, you'll see what I'm trying to do, without me needing to go into too much detail. Let me know if you need more info. Is that syntax possible?
EDIT: See new attempt below
--State - If suburb/postcode, could use postcode lookup
Declare #Missing as nvarchar(255),
#MissingUpdate as nvarchar(255),
#MatchA as nvarchar(255),
#MatchB as nvarchar(255),
#Reason as nvarchar(255);
Set #Missing = '[StateEXPORT]'; -- field to update
Set #MissingUpdate = '[State]'; -- field in postcode lookup to pull in
Set #MatchA = '[PostcodeEXPORT]'; -- field in master field to match with
Set #MatchB = '[Pcode]'; -- field in postcode lookup to match with
Set #Reason = 'Contactable - Needs verificiation - #MissingUpdate taken from Lookup'; -- reason here
update [BT].[dbo].[test]
set #Missing = b.#MissingUpdate,
FinalPot = #Reason
FROM [BT].[dbo].[test] a
INNER JOIN [BT].[dbo].[Postcode Lookup] b
ON a.#MatchA = b.#MatchB
where (#Missing is null or #Missing = '0') and [AddressSource] != ('Uncontactable')
GO
EDIT: SECOND ATTEMPT:
set #sql = 'update [BT].[dbo].[test] set ' + quotename(#Missing) + '= b.' + quotename(#MissingUpdate) + ', FinalPot = ' + #Reason + 'FROM [BT].[dbo].[test] a INNER JOIN [BT].[dbo].[Postcode Lookup] b ON a.' + quotename(#MatchA) + ' = b.' + quotename(#MatchB) + 'where (' + quotename(#Missing) + 'is null or' + quotename(#Missing) + ' = 0 and [AddressSource] != "(Uncontactable)"'
exec (#sql)
Thanks for your help,
Lucas
No, this syntax is not possible, at least not directly: you need to specify the column name, not a string variable that has the name.
If you wish to decide the names of columns dynamically, you could make a SQL string that represents the statement that you wish to execute, and pass that string to EXECUTE command. You have to take extra care not to put any of the user-entered data into the generated SQL string, though, to avoid SQL injection attacks.
EDIT: The reason your second attempt may be failing is that you are passing names in square brackets to quotename. You should remove brackets from your variable declarations, like this:
Set #Missing = 'StateEXPORT'; -- field to update
Set #MissingUpdate = 'State'; -- field in postcode lookup to pull in
Set #MatchA = 'PostcodeEXPORT'; -- field in master field to match with
Set #MatchB = 'Pcode'; -- field in postcode lookup to match with
You can't use variable names as column names without dynamic SQL.
An example of a dynamic SQL query:
declare #ColumnName varchar(100) = 'col1'
declare #sql varchar(max)
set #sql = 'select ' + quotename(#ColumnName) + ' from dbo.YourTable'
exec (#sql)
I'm doing a SQLBulkCopy from my web app and inserting the records into a staging table. This is my first time working with staging tables. The live table that will be accepting the data has about 200 fields and can change into the future. When this change occurs I didn't want to have to re-write the merge statement.
I came up with this SQL that mimics the merge functionality, but doesn't require me to spell out the table columns. I am not an SQL expert and wanted someone that is to take a look and let me know if you see any problems that could arise by using this SQL because I haven't seen any examples of this and many people searching.
Note that records in the staging table that have a null id field are to be inserted.
-- set the table names, primary key field & vars to hold query parts
DECLARE #LiveTable varchar(20) = 'Test'
DECLARE #StagingTable varchar(20) = 'TestStaging'
DECLARE #PKField varchar(20) = 'TestPK'
DECLARE #SQLSet nvarchar(MAX) = ''
DECLARE #SQLInsertFields nvarchar(MAX) = ''
-- get comma delimited field names
DECLARE #Fields nvarchar(MAX) = (SELECT dbo.fn_GetCommaDelimitedFieldNames(#LiveTable))
-- loop through fields generating set clause of query to execute
WHILE LEN(#Fields) > 0
BEGIN
DECLARE #Field varchar(50) = left(#Fields, CHARINDEX(',', #Fields+',')-1)
IF #Field <> #PKField -- the primary key field cannot be updated
BEGIN
SET #SQLSet += ', ' + #LiveTable + '.' + #Field + ' = ' + #StagingTable + '.' + #Field
SET #SQLInsertFields += ', ' + #Field
END
SET #Fields = STUFF(#Fields, 1, CHARINDEX(',', #Fields+','), '')
END
-- remove the leading comma
SET #SQLSet = SUBSTRING(#SQLSet,3,LEN(#SQLSet))
SET #SQLInsertFields = SUBSTRING(#SQLInsertFields,3,LEN(#SQLInsertFields))
-- update records from staging table where primary key is provided
DECLARE #SQL nvarchar(MAX) = N'UPDATE ' + #LiveTable +
' SET ' + #SQLSet +
' FROM ' + #LiveTable +
' INNER JOIN ' + #StagingTable +
' ON ' + #LiveTable + '.' + #PKField + ' = ' + #StagingTable + '.' + #PKField
-- insert records from staging table where primary key is null
SET #SQL += '; INSERT INTO ' + #LiveTable + ' (' + #SQLInsertFields + ') SELECT ' + #SQLInsertFields + ' FROM ' + #StagingTable + ' WHERE ' + #PKField + ' IS NULL'
-- delete the records from the staging table
SET #SQL += '; DELETE FROM ' + #StagingTable
-- execute the sql statement to update existing records and insert new records
exec sp_executesql #SQL;
If anyone see's any issues with performance or anything else, I appreciate the insight.
Don't do this. Really. You're working very hard to avoid a rare problem that you probably won't handle correctly when the time comes.
If the target table changes, how do you know it will change in such a way that your fancy dynamic SQL will work correctly? How can you be sure it won't seem to work -- i.e. will work, syntactically -- but actually do the wrong thing? If and when the target table changes, won't you have to change your application, and the staging table too? With all that in the air, what's adding one more SET clause?
In the meanwhile, how can anyone be expected to read that gobbledygook (not your fault, really, that's SQL's syntax)? A bog-standard insert statement would be very clear and dependable.
And fast. SQL Server can't optimize your dynamic query. You used bcp for efficiency, and now you're defeating it with well meaning futureproofingness.