Parameterizing XPath for modify() in SQL Server XML Processing - sql-server

Just like the title suggests, I'm trying to parameterize the XPath for a modify() method for an XML data column in SQL Server, but running into some problems.
So far I have:
DECLARE #newVal varchar(50)
DECLARE #xmlQuery varchar(50)
SELECT #newVal = 'features'
SELECT #xmlQuery = 'settings/resources/type/text()'
UPDATE [dbo].[Users]
SET [SettingsXml].modify('
replace value of (sql:variable("#xmlQuery"))[1]
with sql:variable("#newVal")')
WHERE UserId = 1
with the following XML Structure:
<settings>
...
<resources>
<type> ... </type>
...
</resources>
...
</settings>
which is then generating this error:
XQuery [dbo.Users.NewSettingsXml.modify()]: The target of 'replace' must be at most one node, found 'xs:string ?'
Now I realize that the modify method must not be capable of accepting a string as a path, but is there a way to accomplish this short of using dynamic SQL?
Oh, by the way, I'm using SQL Server 2008 Standard 64-bit, but any queries I write need to be compatible back to 2005 Standard.
Thanks!

In case anyone was interested, I came up with a pretty decent solution myself using a dynamic query:
DECLARE #newVal nvarchar(max)
DECLARE #xmlQuery nvarchar(max)
DECLARE #id int
SET #newVal = 'foo'
SET #xmlQuery = '/root/node/leaf/text()'
SET #id = 1
DECLARE #query nvarchar(max)
SET #query = '
UPDATE [Table]
SET [XmlColumn].modify(''
replace value of (' + #xmlQuery + '))[1]
with sql:variable("#newVal")'')
WHERE Id = #id'
EXEC sp_executesql #query,
N'#newVal nvarchar(max) #id int',
#newVal, #id
Using this, the only unsafe part of the dynamic query is the xPath, which, in my case, is controlled entirely by my code and so shouldn't be exploitable.

The best I could figure out was this:
declare #Q1 varchar(50)
declare #Q2 varchar(50)
declare #Q3 varchar(50)
set #Q1 = 'settings'
set #Q2 = 'resources'
set #Q3 = 'type'
UPDATE [dbo].[Users]
SET [SettingsXml].modify('
replace value of (for $n1 in /*,
$n2 in $n1/*,
$n3 in $n2/*
where $n1[local-name(.) = sql:variable("#Q1")] and
$n2[local-name(.) = sql:variable("#Q2")] and
$n3[local-name(.) = sql:variable("#Q3")]
return $n3/text())[1]
with sql:variable("#newVal")')
WHERE UserId = 1
Node names are parameters but the level/number of nodes is sadly not.

Here is the solution we found for parameterizing both the property name to be replaced and the new value. It needs a specific xpath, and the parameter name can be an sql variable or table column.
SET Bundle.modify
(
'replace value of(//config-entry-metadata/parameter-name[text() = sql:column("BTC.Name")]/../..//value/text())[1] with sql:column("BTC.Value") '
)
This is the hard coded x path: //config-entry-metadata/parameter-name ... /../..//value/text()
The name of the parameter is dynamic: [text() = sql:column("BTC.Name")]
The new value is also dynamic: with sql:column("BTC.Value")

Related

Need to mix dynamic SQL, Open Query, JSON, dynamic variables, and a few other oddities into a single query

Need to run dynamic SQL against DB2 on MS SQL through OpenQuery, get results back in JSON, then return this as an Output Parameter in a Stored Procedure
I've tried using a table variable as the sample code shows, but I get this error:
The FOR JSON clause is not allowed in a INSERT statement
I've also tried wrapping the query into a CTE, but given the JSON column name changes I can't use * or I get this error:
No column name was specified for column 1 of 'tbl'.
So I'm at a loss. I need to run this and get the JSON in the Output parameter, but given I'm having to mix a call to DB2 through OpenQuery and dynamic SQL to set the parameter I can't find a syntax that works.
create procedure uspTesting (
#inAccountNumber nvarchar(20),
#outJSON nvarchar(max) output)
as
begin declare #result table (ResultJson nvarchar(max));
declare #tsql nvarchar(4000) = '
select name, age
from openquery(db2link,''
select name,
age
from db2.account
where accountnumber = ''''' + #inAccountNumber + ''''')'') tbl for json auto';
insert into #result
EXEC (#TSQL);
select #outJSON = ResultJson from #result; End
The results I'm looking for are the JSON string in the output parameter #outJSON.
Apply the FOR JSON after you've gotten the data, load it into a temp table and then use the FOR JSON.
Without test data, etc you might have to adjust this, but try something like:
CREATE PROCEDURE [uspTesting]
(
#inAccountNumber NVARCHAR(20)
, #outJSON NVARCHAR(MAX) OUTPUT
)
AS
BEGIN
DECLARE #result TABLE
(
[name] NVARCHAR(100) --whatever data type you need here
, [age] NVARCHAR(100)
);
DECLARE #tsql NVARCHAR(4000) = '
select name, age
from openquery(db2link,''
select name,
age
from db2.account
where accountnumber = ''' + #inAccountNumber + ''')';
--Here we will just load a table variable with the data.
INSERT INTO #result
EXEC ( #tsql );
--Then we will select from that table variable applying the JSON here.
SET #outJSON = (
SELECT *
FROM #result
FOR JSON AUTO
);
END;

SQL Server: get column value from string column name, assign value to variable

In SQL Server in a stored procedure, I want to get the value in a column of a single-row table given the column name, and assign that value to a variable. The column name may be different every time (I use T-SQL to interrogate the schema at run time).
The example given below is as minimal as I can make it, the important thing is that you cannot assume that the column name will always be entity_client, it could be anything at all, though (due to interrogation of INFORMATION SCHEMA) we will have the value assigned to the variable #entity_column_name.
Example preparation SQL:
IF OBJECT_ID('tempdb..#foo') IS NOT NULL
BEGIN;
DROP TABLE #foo;
END;
CREATE TABLE #foo
(
id INT,
entity_client NVARCHAR(255)
);
INSERT INTO #foo VALUES (1, 'clientcode|client_number');
DECLARE #entity_column_name NVARCHAR(255) = 'entity_client';
DECLARE #entity_column_value NVARCHAR(255);
I have tried the following:
SELECT TOP 1 #entity_column_name = [#entity_column_value]
FROM #foo;
...which generates an error
Invalid column name '#entity_column_value'
I have also tried:
EXEC('SELECT TOP 1 #entity_column_value = [' + #entity_column_name + '] FROM #foo;');
which generates another error
Must declare the scalar variable "#entity_column_value"
The following works, but unfortunately the column name is hard-coded - I wanted to be able to vary the column name:
SELECT TOP 1 #entity_column_value = [entity_client]
FROM #foo;
Yes, I have looked on SO and found the following questions, but they do not provide an answer where the value is assigned to a variable, in both cases the SELECT output is simply dumped to screen:
Get column value from string column name sql
Get column value from dynamically achieved column name
This will actually work but you need to declare the output variable:
DECLARE #entity_column_name NVARCHAR(255) = 'entity_client';
DECLARE #entity_column_value NVARCHAR(255);
DECLARE #tsql NVARCHAR(1000) = 'SELECT TOP 1 #entity_column_value = [' + #entity_column_name + '] FROM #foo;'
EXEC sp_executesql #tsql, N'#entity_column_value NVARCHAR(255) OUTPUT',
#entity_column_value OUTPUT;

Reading specific value with Openrowset in SQL Server 2016

The aim here is to read a specific value from a different server and store the return value in a local parameter for use later.
Here is the error code:
Msg 102, Level 15, State 1, Line 3
Incorrect syntax near 'Alarm'.
Here is the code I have tried:
declare #sql_string nvarchar(400);
declare #inhostnamn nvarchar(100) = 'BLUE65\SQLEXPRESS'
declare #inuser nvarchar(50) = 'dev1'
declare #password1 nvarchar(50) = 'dev1'
declare #database nvarchar(100) = 'Test_destroy'
declare #count_posts varchar(10)
declare #tabellnamn varchar(50) = 'Alarms'
declare #last_read_alarm varchar(30)
set #tabellnamn = 'Logg'
set #sql_string = N'set #last_read_alarm1 = cast(last_read as nvarchar(30)) select * from openrowset (''SQLNCLI'', ''Server='+#inhostnamn+';UID='+#inuser+';Pwd='+#password1+';Database='+#database+';Persist Security Info=True'',''select Last_ID FROM '+#database +'.dbo.Logg where Tables_sql=''Alarm'' '')';
print 'string =' + #sql_string;
exec sp_executesql #sql_string, N'#last_read_alarm1 varchar(30) OUTPUT', #last_read_alarm1=#last_read_alarm OUTPUT;
select #last_read_alarm
print #last_read_alarm;
Now I am stuck. I cannot see the error I have made, and am hoping for a couple of different eyes.
Thanks to Andrei Odegov for great assistance. It helped putting 4 ' on each side.
The correct code would be in my case now:
set #sql_string = N'select #last_read_alarm= (select * from openrowset (''SQLNCLI'', ''Server='+#inhostnamn+';UID='+#inuser+';Pwd='+#password1+';Database='+#database+';Persist Security Info=True'',''select Last_ID FROM '+#database +'.dbo.Logg where Tables_sql='''''+#tmp_str+''''' ''))';
So the answer from the query will now end up in local variable #last_read_alarm.

Dynamic SQL with Dynamic Parameters

I am trying to write dynamic SQL that can accept up to 50 parameters. It can be 1 or 10 or 15. The limit is 50 from the main sproc but the input to the dynamic sql is not defined.
I have created this dynamic statement through a while loop. It will read the number of parameter in a table(3 in this example) and generates the statement below. I am not concerned with the performance of the while loop because the tables/input are very small:
--generated from while loop
DECLARE #rParam1 NVARCHAR(MAX)
DECLARE #rParam2 NVARCHAR(MAX)
DECLARE #rParam3 NVARCHAR(MAX)
SELECT #rParam1= Parameter_Name FROM [Template_Params] WHERE Key=12345 AND Sort_Order=1
SELECT #rParam2= Parameter_Name FROM [Template_Params] WHERE Key=12345 AND Sort_Order=2
SELECT #rParam3= Parameter_Name FROM [Template_Params] WHERE Key=12345 AND Sort_Order=3
DECLARE #BODY NVARCHAR(MAX);
SELECT #BODY = Body FROM [Template] WHERE Key=12345
SET #Body = REPLACE(#Body,#rparam1,#PARAM_Input1)
SET #Body = REPLACE(#Body,#rparam2,#PARAM_Input2)
SET #Body = REPLACE(#Body,#rparam3,#PARAM_Input3)
SELECT #BODY
This so far works good.
The stored procedure can have 50 inputs but for example I will limit to 5 (only used 3)
--this is from the main sproc
DECLARE #PARAM_Input1 NVARCHAR(MAX)='test',
#PARAM_Input2 NVARCHAR(MAX)='test1',
#PARAM_Input3 NVARCHAR(MAX)='test2',
#PARAM_Input4 NVARCHAR(MAX),
#PARAM_Input5 NVARCHAR(MAX),
#param_input6 nvarchar(max)
Now I need to get these values into the dynamic SQL so my final Dynamic statement should have
SET #Body = REPLACE(#Body,#rparam1,'test')
SET #Body = REPLACE(#Body,#rparam2,'test1')
SET #Body = REPLACE(#Body,#rparam3,'test2')
This is where I am stuck, I do not want to write the REPLACE Statement 50 times like this.
SET #SQL=REPLACE(#SQL,'#PARAM_Input1',''''+#PARAM_Input1+'''')
I tried a cursor but the name of the variable has to be dynamic and it's not in the same scope so the values from the sproc are not being passed.
It might not be possible in SQL to do what you have in mind. A solution could be if you can send a table as parameter to contain the information as below (or generate it dinamically:
ParameterName | Value
<value of #rParam1> | <value of #Param_Input1>
<value of #rParam2> | <value of #PARAM_Input2>
Then, update #BODY variable
SELECT #BODY = Body FROM [Template] WHERE Key=12345
UPDATE <TableParameter_Or_Variable>
SET #Body = REPLACE(#Body, ParameterName, Value)
I don't know how you obtain the values for #PARAM_InputX, but for `#rParamX' you can do something like:
INSERT INTO #Table
SELECT Parameter_Name AS ParameterName, null AS Value
[Template_Params] WHERE Key=12345
ORDER BY sort_order
and then update the rows accordingly. On #Table you can open a cursor and match the current row to #PARAM_InputX.

how can I use a sproc #param as column alias?

How can use a sproc param as a column alias? For example:
SELECT '12345' AS #MySprocParam
I tried the following sql but it doesn't work:
DECLARE #MySprocParam VARCHAR(50) = 'TestAlias'
SELECT 'ASDF' AS #MyProcParam
You need dynamic sql
DECLARE #MySprocParam VARCHAR(50) = 'TestAlias'
SET #MyProcParam = QUOTENAME(#MyProcParam)
exec( 'SELECT ''ASDF'' AS '+#MyProcParam)

Resources