I have the following code to create a SQL function that will parse an XML string and create a table of key value pairs that represent the nodes and values. This works fine for me in my use cases.
CREATE FUNCTION XmlToKeyValue
(
#rootName AS varchar(256),
#xml AS Xml
)
RETURNS #keyval TABLE ([key] varchar(max), [value] varchar(max))
AS
BEGIN
DECLARE #input TABLE (XmlData XML NOT NULL)
INSERT INTO #input VALUES(#xml)
INSERT #keyval ([key], [value])
SELECT
XC.value('local-name(.)', 'varchar(max)') AS [key],
XC.value('(.)[1]', 'varchar(max)') AS [value]
FROM
#input
CROSS APPLY
XmlData.nodes('/*[local-name()=sql:variable("#rootName")]/*') AS XT(XC)
RETURN
END
What I am trying to do is have a stored procedure in my main database that will create another database with all the appropriate functions/procedures/etc. So in that stored procedure I am trying to do something like this:
SET #cmd = '
CREATE FUNCTION XmlToKeyValue
(
#rootName AS varchar(256),
#xml AS Xml
)
RETURNS #keyval TABLE ([key] varchar(max), [value] varchar(max))
AS
BEGIN
DECLARE #input TABLE (XmlData XML NOT NULL)
INSERT INTO #input VALUES(#xml)
INSERT #keyval ([key], [value])
SELECT
XC.value(''local-name(.)'', ''varchar(max)'') AS [key],
XC.value(''(.)[1]'', ''varchar(max)'') AS [value]
FROM
#input
CROSS APPLY
XmlData.nodes(''/*[local-name()=sql:variable("#rootName")]/*'') AS XT(XC)
RETURN
END
'
BEGIN TRY
EXEC(N'USE '+#dbName+'; EXEC sp_executesql N''' + #cmd + '''; USE master')
END TRY
BEGIN CATCH
PRINT 'Error creating XmlToKeyValue'
Print Error_Message();
RETURN
END CATCH
However, I am getting the following error that I can't figure out how to resolve.
Error creating XmlToKeyValue
Incorrect syntax near 'local'.
Can I use local-name in a dynamic sql statement? If not, how can I achieve my goal? Thank you.
The problem is not the local-name function. It is entirely the fact that you are concatenating in the #cmd variable into your Dynamic SQL without properly escaping the embedded single-quotes.
This line:
EXEC(N'USE '+#dbName+'; EXEC sp_executesql N''' + #cmd + '''; USE master')
should be:
SET #cmd = REPLACE(#cmd, N'''', N'''''');
EXEC(N'USE ' + #dbName + N'; EXEC sp_executesql N''' + #cmd + N''';');
Else you are embedding:
XC.value(''local-name(
into the string, but using the same number of escape sequences, hence the XC.value( now becomes the end of the string and the local-name(.) is technically unescaped SQL and not part of a string.
Also:
You don't need the USE master at the end of the Dynamic SQL (so I removed it).
You prefixed the first string literal with N but none of the others (I added the N in for the others so that they all have that prefix).
Related
I'm using T-SQL. The goal is to insert multiples files into a database.
If I'm using without a loop, it's working fine.
In the loop, I always get this error:
#InputXML should be declared
My code:
IF OBJECT_ID('TEMPDB..#TEMP_FILES') IS NOT NULL
DROP TABLE #TEMP_FILES
CREATE TABLE #TEMP_FILES
(
FileName VARCHAR(MAX),
DEPTH VARCHAR(MAX),
[FILE] VARCHAR(MAX)
)
INSERT INTO #TEMP_FILES
EXEC master.dbo.xp_DirTree '\\MyServer\MyFolder\',1,1
DELETE FROM #TEMP_FILES WHERE RIGHT(FileName,4) != '.XML'
--
SET QUOTED_IDENTIFIER ON
GO
TRUNCATE Table MyTable2
DECLARE #InputXML XML
DECLARE #FILENAME VARCHAR(MAX),#SQL VARCHAR(MAX)
WHILE EXISTS(SELECT * FROM #TEMP_FILES)
BEGIN
SET #FILENAME = (SELECT TOP 1 FileName FROM #TEMP_FILES)
SET #sql = 'SELECT #InputXML = CAST(x AS XML) FROM OPENROWSET(BULK \\MyServer\MyFolder\'''+ #FILENAME +''', SINGLE_BLOB) AS T(x)
INSERT INTO MyTable2 ([id],[version], [name], [listId], [listCode])
SELECT
product.value(''(#id)[1]'', ''NVARCHAR(10)''),
product.value(''(#version)[1]'', ''NVARCHAR(14)''),
product.value(''(name[1])'', ''NVARCHAR(255)''),
product.value(''(listId[1])'', ''NVARCHAR(9)''),
product.value(''(listCode[1])'', ''NVARCHAR(10)'')
FROM #InputXML.nodes(''xxx/values/value'') AS X(product)'
EXEC(#SQL)
DELETE FROM #TEMP_FILES
WHERE FileName = #FILENAME
END
You need to declare the variable inside the dynamic SQL (which should be nvarchar not varchar). You should also use QUOTENAME to ensure no issues with the filename:
DECLARE #sql nvarchar(max) = N'
DECLARE #InputXML XML;
SELECT #InputXML = CAST(x AS XML) FROM OPENROWSET(BULK ' + QUOTENAME(N'\\MyServer\MyFolder\' + #FILENAME, '''') + N', SINGLE_BLOB) AS T(x)
INSERT INTO MyTable2 ([id],[version], [name], [listId], [listCode])
SELECT
product.value(''(#id)[1]'', ''NVARCHAR(10)''),
product.value(''(#version)[1]'', ''NVARCHAR(14)''),
product.value(''(name[1])'', ''NVARCHAR(255)''),
product.value(''(listId[1])'', ''NVARCHAR(9)''),
product.value(''(listCode[1])'', ''NVARCHAR(10)'')
FROM #InputXML.nodes(''xxx/values/value'') AS X(product)'
I will say though, that I urge you to find another method to load files into SQL Server. Dynamic OPENROWSET, especially from user input, is not advisable. Bulk Insert or BCP may be an option.
I have a form with 3 check box dropdown lists enabling multiple selection from each control.
Lets say for talking sake its an accommodation table I am querying and the check box dropdown lists are 'AccommodationName', 'Company', and 'Nights'.
So potentially I could be passing in multiple values from each control and I want to return an aggregated query relevant to all data input.
How should I be going about this query?
Is the query going to have to be dynamic sql?
Please note, I am using sql server 2005.
You will need to create a split function inside you database,
Definition Of Split Function
CREATE FUNCTION [dbo].[split]
(
#delimited NVARCHAR(MAX),
#delimiter NVARCHAR(100)
)
RETURNS #t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
Stored Procedure
Then you need to create a stored procedure which will build sql query dynamically and use this split function to handle multiple values passed as a comma deliminated list.
CREATE PROCEDURE GetData
#AccommodationName VARCHAR(1000) = NULL,
#Company VARCHAR(1000) = NULL,
#Nights VARCHAR(1000) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQL NVARCHAR(MAX);
SET #SQL = N' SELECT * FROM TableName WHERE 1 = 1 '
+ CASE WHEN #AccommodationName IS NOT NULL
THEN N' AND AccommodationName IN (SELECT Val FROM dbo.split(#AccommodationName )) '
ELSE N'' END
+ CASE WHEN #Company IS NOT NULL
THEN N' AND Company IN (SELECT Val FROM dbo.split(#Company)) '
ELSE N'' END
+ CASE WHEN #Nights IS NOT NULL
THEN N' AND Nights IN (SELECT Val FROM dbo.split(#Nights)) '
ELSE N'' END
EXECUTE sp_executesql #SQL
,N'#AccommodationName VARCHAR(1000), #Company VARCHAR(1000), #Nights VARCHAR(1000)'
,#AccommodationName
,#Company
,#Nights
END
I have some tables for storing different file information, like thumbs, images, datasheets, ...
I'm writing a stored procedure to retrieve filename of a specific ID. something like:
CREATE PROCEDURE get_file_name(
#id int,
#table nvarchar(50)
)as
if #table='images'
select [filename] from images
where id = #id
if #table='icons'
select [filename] from icons
where id = #id
....
How can I rewrite this procedure using case when statement or should I just use table name as variable?
You can't use case .. when to switch between a table in the FROM clause (like you can in a conditional ORDER BY). i.e. so the following:
select * from
case when 1=1
then t1
else t2
end;
won't work.
So you'll need to use dynamic SQL. It's best to parameterize the query as far as possible, for example the #id value can be parameterized:
-- Validate #table is E ['images', 'icons', ... other valid names here]
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'select [filename] from **TABLE** where id = #id';
SET #sql = REPLACE(#sql, '**TABLE**', #table);
sp_executesql #sql, N'#id INT', #id = #id;
As with all dynamic Sql, note that unparameterized values which are substituted into the query (like #table), make the query vulnerable to Sql Injection attacks. As a result, I would suggest that you ensure that #table comes from a trusted source, or better still, the value of #table is compared to a white list of permissable tables prior to execution of the query.
Just build SQL string in another variable and EXECUTE it
DECLARE #sql AS NCHAR(500)
SET #sql=
'SELECT [filename] '+
' FROM '+#table+
' WHERE id = #id'
EXECUTE(#sql)
CREATE PROCEDURE get_file_name(
#id int,
#table nvarchar(50)
)as
DECLARE #SQL nvarchar(max);
SET #SQL = 'select [filename] from ' + #table + ' where id = ' + #id
EXECUTE (#SQL)
I have:
a table with an xml type column (list of IDs)
an xml type parameter (also list of IDs)
What is the best way to remove nodes from the column that match the nodes in the parameter, while leaving any unmatched nodes untouched?
e.g.
declare #table table (
[column] xml
)
insert #table ([column]) values ('<r><i>1</i><i>2</i><i>3</i></r>')
declare #parameter xml
set #parameter = '<r><i>1</i><i>2</i></r>'
-- this is the problem
update #table set [column].modify('delete (//i *where text() matches #parameter*)')
The MSDN documentation indicates it should be possible (in Introduction to XQuery in SQL Server 2005):
This stored procedure can easily be
modified to accept an XML fragment
which contains one or more skill
elements thereby allowing the user to
delete multiple skill nodes with a
single invocation of stored procedure.
While the delete is a little awkward to do this way, you can instead do an update to change the data, provided your data is simple (such as the example you gave). The following query will basically split the two XML strings into tables, join them, exclude the non-null (matching) values, and convert it back to XML:
UPDATE #table
SET [column] = (
SELECT p.i.value('.','int') AS c
FROM [column].nodes('//i') AS p(i)
OUTER APPLY (
SELECT x.i.value('.','bigint') AS i
FROM #parameter.nodes('//i') AS x(i)
WHERE p.i.value('.','bigint') = x.i.value('.','int')
) a
WHERE a.i IS NULL
FOR XML PATH(''), TYPE
)
You need something in the form:
[column].modify('delete (//i[.=(1, 2)])') -- like SQL IN
-- or
[column].modify('delete (//i[.=1 or .=2])')
-- or
[column].modify('delete (//i[.=1], //i[.=2])')
-- or
[column].modify('delete (//i[contains("|1|2|",concat("|",.,"|"))])')
XQuery doesn't support xml SQL types in SQL2005, and the modify method only accepts string literals (no variables allowed).
Here's an ugly hack w/ the contains function:
declare #table table ([column] xml)
insert #table ([column]) values ('<r><i>1</i><i>2</i><i>3</i></r>')
declare #parameter xml
set #parameter = '<r><i>1</i><i>2</i></r>'
-- build a pipe-delimited string
declare #in nvarchar(max)
set #in = convert(nvarchar(max),
#parameter.query('for $i in (/r/i) return concat(string($i),"|")')
)
set #in = '|'+replace(#in,'| ','|')
update #table set [column].modify ('
delete (//i[contains(sql:variable("#in"),concat("|",.,"|"))])
')
select * from #table
Here's another w/ dynamic SQL:
-- replace table variable with temp table to get around variable scoping
if object_id('tempdb..#table') is not null drop table #table
create table #table ([column] xml)
insert #table ([column]) values ('<r><i>1</i><i>2</i><i>3</i></r>')
declare #parameter xml
set #parameter = '<r><i>1</i><i>2</i></r>'
-- we need dymamic SQL because the XML modify method only permits string literals
declare #sql nvarchar(max)
set #sql = convert(nvarchar(max),
#parameter.query('for $i in (/r/i) return concat(string($i),",")')
)
set #sql = substring(#sql,1,len(#sql)-1)
set #sql = 'update #table set [column].modify(''delete (//i[.=('+#sql+')])'')'
print #sql
exec (#sql)
select * from #table
if you are updating an xml variable rather than a column, use sp_executesql and output parameters:
declare #xml xml
set #xml = '<r><i>1</i><i>2</i><i>3</i></r>'
declare #parameter xml
set #parameter = '<r><i>1</i><i>2</i></r>'
declare #sql nvarchar(max)
set #sql = convert(nvarchar(max),
#parameter.query('for $i in (/r/i) return concat(string($i),",")')
)
set #sql = substring(#sql,1,len(#sql)-1)
set #sql = 'set #xml.modify(''delete (//i[.=('+#sql+')])'')'
exec sp_executesql #sql, N'#xml xml output', #xml output
select #xml
Alternate method using a cursor to iterate through delete values, probably less efficient due to multiple updates:
declare #table table ([column] xml)
insert #table ([column]) values ('<r><i>1</i><i>2</i><i>3</i></r>')
declare #parameter xml
set #parameter = '<r><i>1</i><i>2</i></r>'
/*
-- unfortunately, this doesn't work:
update t set [column].modify('delete (//i[.=sql:column("p.i")])')
from #table t, (
select i.value('.', 'nvarchar')
from #parameter.nodes('//i') a (i)
) p (i)
select * from #table
*/
-- so we have to use a cursor
declare #cursor cursor
set #cursor = cursor for
select i.value('.', 'varchar') as i
from #parameter.nodes('//i') a (i)
declare #i int
open #cursor
while 1=1 begin
fetch next from #cursor into #i
if ##fetch_status <> 0 break
update #table set [column].modify('delete (//i[.=sql:variable("#i")])')
end
select * from #table
More information on the limitations of XML variables in SQL2005 here:
http://blogs.msdn.com/denisruc/archive/2006/05/17/600250.aspx
Does anyone have a better way?
The code is as follows:
ALTER PROCEDURE dbo.pdpd_DynamicCall
#SQLString varchar(4096) = null
AS
Begin
create TABLE #T1 ( column_1 varchar(10) , column_2 varchar(100) )
insert into #T1
execute ('execute ' + #SQLString )
select * from #T1
End
The problem is that I want to call different procedures that can give back different columns.
Therefore I would have to define the table #T1 generically.
But I don't know how.
Can anyone help me on this problem?
Try:
SELECT into #T1 execute ('execute ' + #SQLString )
And this smells real bad like an sql injection vulnerability.
correction (per #CarpeDiem's comment):
INSERT into #T1 execute ('execute ' + #SQLString )
also, omit the 'execute' if the sql string is something other than a procedure
You can define a table dynamically just as you are inserting into it dynamically, but the problem is with the scope of temp tables. For example, this code:
DECLARE #sql varchar(max)
SET #sql = 'CREATE TABLE #T1 (Col1 varchar(20))'
EXEC(#sql)
INSERT INTO #T1 (Col1) VALUES ('This will not work.')
SELECT * FROM #T1
will return with the error "Invalid object name '#T1'." This is because the temp table #T1 is created at a "lower level" than the block of executing code. In order to fix, use a global temp table:
DECLARE #sql varchar(max)
SET #sql = 'CREATE TABLE ##T1 (Col1 varchar(20))'
EXEC(#sql)
INSERT INTO ##T1 (Col1) VALUES ('This will work.')
SELECT * FROM ##T1
Hope this helps,
Jesse
Be careful of a global temp table solution as this may fail if two users use the same routine at the same time as a global temp table can be seen by all users...
create a global temp table with a GUID in the name dynamically. Then you can work with it in your code, via dyn sql, without worry that another process calling same sproc will use it. This is useful when you dont know what to expect from the underlying selected table each time it runs so you cannot created a temp table explicitly beforehand. ie - you need to use SELECT * INTO syntax
DECLARE #TmpGlobalTable varchar(255) = 'SomeText_' + convert(varchar(36),NEWID())
-- select #TmpGlobalTable
-- build query
SET #Sql =
'SELECT * INTO [##' + #TmpGlobalTable + '] FROM SomeTable'
EXEC (#Sql)
EXEC ('SELECT * FROM [##' + #TmpGlobalTable + '] ')
EXEC ('DROP TABLE [##' + #TmpGlobalTable + ']')
PRINT 'Dropped Table ' + #TmpGlobalTable
INSERT INTO #TempTable
EXEC(#SelectStatement)
Try Below code for creating temp table dynamically from Stored Procedure Output using T-SQL
declare #ExecutionName varchar(1000) = 'exec [spname] param1,param2 '
declare #sqlStr varchar(max) = ''
declare #tempTableDef nvarchar(max) =
(
SELECT distinct
STUFF(
(
SELECT ','+a.[name]+' '+[system_type_name]
+'
' AS [text()]
FROM sys.dm_exec_describe_first_result_set (#ExecutionName, null, 0) a
ORDER BY a.column_ordinal
FOR XML PATH ('')
), 1, 1, '') tempTableDef
FROM sys.dm_exec_describe_first_result_set (#ExecutionName, null, 0) b
)
IF ISNULL(#tempTableDef ,'') = '' RAISERROR( 'Invalid SP Configuration. At least one column is required in Select list of SP output.',16,1) ;
set #tempTableDef='CREATE TABLE #ResultDef
(
' + REPLACE(#tempTableDef,'
','') +'
)
INSERT INTO #ResultDef
' + #ExecutionName
Select #sqlStr = #tempTableDef +' Select * from #ResultDef '
exec(#sqlStr)
DECLARE #EmpGroup INT =3 ,
#IsActive BIT=1
DECLARE #tblEmpMaster AS TABLE
(EmpCode VARCHAR(20),EmpName VARCHAR(50),EmpAddress VARCHAR(500))
INSERT INTO #tblEmpMaster EXECUTE SPGetEmpList #EmpGroup,#IsActive
SELECT * FROM #tblEmpMaster
CREATE PROCEDURE dbo.pdpd_DynamicCall
AS
DECLARE #SQLString_2 NVARCHAR(4000)
SET NOCOUNT ON
Begin
--- Create global temp table
CREATE TABLE ##T1 ( column_1 varchar(10) , column_2 varchar(100) )
SELECT #SQLString_2 = 'INSERT INTO ##T1( column_1, column_2) SELECT column_1 = "123", column_2 = "MUHAMMAD IMRON"'
SELECT #SQLString_2 = REPLACE(#SQLString_2, '"', '''')
EXEC SP_EXECUTESQL #SQLString_2
--- Test Display records
SELECT * FROM ##T1
--- Drop global temp table
IF OBJECT_ID('tempdb..##T1','u') IS NOT NULL
DROP TABLE ##T1
End
Not sure if I understand well, but maybe you could form the CREATE statement inside a string, then execute that String? That way you could add as many columns as you want.