SQL XML modify replace - sql-server

I need help in replacing the XML tag value. Sample code is as follows:
declare #l_runtime_xml XML
declare #l_n_DrillRepID numeric(10)
declare #griddrillparam nvarchar(30)
declare #l_s_DrillBtColumnTag nvarchar(256)
declare #l_s_BTNameSecond nvarchar(30)
set #l_n_DrillRepID =1538
set #griddrillparam = 'userID'
set #l_s_DrillBtColumnTag = 'V_userID'
set #l_s_BTNameSecond = 'l_s_userID'
declare #l_runtime_xmlAAA nvarchar(max)
set #l_runtime_xmlAAA = N'<REPORT_RUNTIME_XML><USER_ID>AISHU</USER_ID><SHEET><SHEET_NO>1</SHEET_NO><DRILLTHRU_PARAM><ENT_RPT><ENT_RPT_ID>1537</ENT_RPT_ID><ENT_RPT_NAME>Reddy111</ENT_RPT_NAME><DEFAULT>1</DEFAULT><CRITERIA><DISPLAY/><HIDDEN/></CRITERIA><COLUMN_HEADER>N</COLUMN_HEADER><DISPLAY_BUTTON>N</DISPLAY_BUTTON><PARAMLIST><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>userID</NAME><BT_NAME/><V_userID>(none)</V_userID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>langID</NAME><BT_NAME/><V_langID>(none)</V_langID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>l_s_userID</NAME><BT_NAME/><V_l_s_userID>(none)</V_l_s_userID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>a_i_langID</NAME><BT_NAME/><V_a_i_langID>(none)</V_a_i_langID></PARAM></PARAMLIST></ENT_RPT><ENT_RPT><ENT_RPT_ID>1538</ENT_RPT_ID><ENT_RPT_NAME>Reddy333</ENT_RPT_NAME><DEFAULT>0</DEFAULT><CRITERIA><DISPLAY/><HIDDEN/></CRITERIA><COLUMN_HEADER>N</COLUMN_HEADER><DISPLAY_BUTTON>N</DISPLAY_BUTTON><PARAMLIST><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>userID</NAME><BT_NAME/><V_userID>(none)</V_userID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>langID</NAME><BT_NAME/><V_langID>(none)</V_langID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>l_s_userID</NAME><BT_NAME/><V_l_s_userID>(none)</V_l_s_userID></PARAM><PARAM><COLTYPE/><POSITION>-999</POSITION><BT_COLUMN_NAME/><NAME>a_i_langID</NAME><BT_NAME/><V_a_i_langID>(none)</V_a_i_langID></PARAM></PARAMLIST></ENT_RPT></DRILLTHRU_PARAM></SHEET></REPORT_RUNTIME_XML>'
select #l_runtime_xml = cast(#l_runtime_xmlAAA as XML)
SET #l_runtime_xml.modify('replace value of ((//SHEET/DRILLTHRU_PARAM/ENT_RPT[(ENT_RPT_ID/text())[1] eq sql:variable("#l_n_DrillRepID")]/PARAMLIST/PARAM[(NAME/text())[1] eq sql:variable("#griddrillparam")]/#l_s_DrillBtColumnTag))[1] with sql:variable("#l_s_BTNameSecond")')
set #l_runtime_xmlAAA = cast(#l_runtime_xml as nvarchar(max))
select #l_runtime_xmlAAA

I think your issue is here:
.../#l_s_DrillBtColumnTag))[1]
If I understand this correctly, you are trying to access an element of the name "V_userId" by putting a variable in that place. But this will not work...
Your question is not quite clear to me (and admittably your structure isn't either, it seems too complicated...). And what do you mean with delete the last child of a node?
Your query would be the following:
Find the "ENT_RPT" with the given number, there find the "PARAM" whose name is what's given and on the same level find the element with the name given, which is to be replaced (see local-name()). Replace this with the value given:
SET #l_runtime_xml.modify('replace value of (//SHEET/DRILLTHRU_PARAM/ENT_RPT[ENT_RPT_ID/text()=sql:variable("#l_n_DrillRepID")]/PARAMLIST/PARAM[NAME/text()=sql:variable("#griddrillparam")]/*[local-name(.)=sql:variable("#l_s_DrillBtColumnTag")]/text())[1] with sql:variable("#l_s_BTNameSecond")')
On the first sigth I assume, that you are dealing with named parameters. It was much better to put the value of these parameters in an element with the same name for all of them (e.g. <VALUE>something</VALUE>. They are specified via "NAME" anyway.
And even better was a structure with attributes, something like
...<PARAM Name="userName" position="-999" moreAttributes...>SomeValue</PARAM>
This would make your navigation much easier and less erronous...

Related

How do I perform a string function whilst using OPENJSON WITH?

In this example, DT_RowId is a concatenated string. I need to extract out its values, and make them available in a WHERE clause (not shown).
Is there a way to perform string functions on a value as part of a FROM OPENJSON WITH?
Is there a proper way to extract concatenated strings from a value without using a clunky SELECT statement?
Side note: This example is REALLY part of an UPDATE statement, so I'd be using the extracted values in the WHERE clause (not shown here). Also, also: Split is a custom string function we have.
BTW: I have full control of that DT_RowId, and i could make it an array, for example, [42, 1, 1]
declare #jsonRequest nvarchar(max) = '{"DT_RowId":"42_1_14","Action":"edit","Schedule":"1","Slot":"1","Period":"9:00 to 9:30 UPDATED","AMOnly":"0","PMOnly":"0","AllDay":"1"}'
select
(select Item from master.dbo.Split(source.DT_RowId, '_', 0) where ItemIndex = 0) as ID
,source.Schedule
,source.Slot
,source.[Period]
,source.AllDay
,source.PMOnly
,source.AMOnly
from openjson(#jsonRequest, '$')
with
(
DT_RowId varchar(255) '$.DT_RowId' /*concatenated string of row being edited */
,Schedule tinyint '$.Schedule'
,Slot tinyint '$.Slot'
,[Period] varchar(20) '$.Period'
,AllDay bit '$.AllDay'
,PMOnly bit '$.PMOnly'
,AMOnly bit '$.AMOnly'
) as source
Using SQL-Server 2016+ offers a nice trick to split a string fast and position-aware:
select
DTRow.AsJson as DTRow_All_Content
,JSON_VALUE(DTRow.AsJson,'$[0]') AS DTRow_FirstValue
,source.Schedule
,source.Slot
,source.[Period]
,source.AllDay
,source.PMOnly
,source.AMOnly
from openjson(#jsonRequest, '$')
with
(
DT_RowId varchar(255) '$.DT_RowId' /*concatenated string of row being edited */
,Schedule tinyint '$.Schedule'
,Slot tinyint '$.Slot'
,[Period] varchar(20) '$.Period'
,AllDay bit '$.AllDay'
,PMOnly bit '$.PMOnly'
,AMOnly bit '$.AMOnly'
) as source
OUTER APPLY(SELECT CONCAT('["',REPLACE([source].DT_RowId,'_','","'),'"]')) DTRow(AsJson);
The magic is the transformation of 42_1_14 to ["42","1","14"] with some simple string methods. With this you can use JSON_VALUE() to fetch an item by its position.
General hint: If you have full control of DT_RowId you should rather create this JSON array right from the start and avoid hacks while reading this...
update
Just to demonstrate how this would run, if the value was a JSON-array, check this out:
declare #jsonRequest nvarchar(max) = '{"DT_RowId":["42","1","14"]}'
select
source.DT_RowId as DTRow_All_Content
,JSON_VALUE(source.DT_RowId,'$[0]') AS DTRow_FirstValue
from openjson(#jsonRequest, '$')
with
(
DT_RowId NVARCHAR(MAX) AS JSON
) as source;
update 2
Just to add a little to your self-answer:
We must think of JSON as a special string. As there is no native JSON data type, the engine does not know, when the string is a string, and when it is JSON.
Using NVARCHAR(MAX) AS JSON in the WITH-clause allows to deal with the return value again with JSON methods. For example, we could use CROSS APPLY OPENJSON(UseTheValueHere) to dive into nested lists and objects.
Actually there's no need to use this at all. If there are no repeating elements, one could just parse all the values directly:
SELECT JSON_VALUE(#jsonRequest,'$.DT_RowId[0]') AS DTRowId_1
,JSON_VALUE(#jsonRequest,'$.Action') AS [Action]
--and so on...
But this would mean to parse the JSON over and over, which is very expensive.
Using OPENJSON means to read the whole JSON in one single pass (on the current level) and return the elements found (with or without a JSON path) in a derived set (one row for each element).
The WITH-clause is meant to perform kind of PIVOT-action and returns the elements as a multi-column-set. The additional advantage is, that you can specify the data type and - if necessary - a differing JSON path and the column's alias.
You can use any valid JSON path (as well in the WITH-clause as in JSON_VALUE() or in many other places). That means that there are several ways to get the same result. Understanding how the engine works, will enable you to find the most performant approach.
OP here. Just expanding on the answer I accepted by Shnugo, with some details and notes... Hopefully all this might help somebody out there.
I am going to make DT_RowId an array
I will use AS JSON for DT_RowId in the OPENJSON WITH statement
I can then treat it as a json structure, and use JSON_VALUE to extract a value at a specific index
declare #jsonRequest nvarchar(max) = '{"DT_RowId":["42", "1", "14"],"Action":"edit","Schedule":"1","Slot":"1","Period":"9:00 to 9:30 UPDATED","AMOnly":"0","PMOnly":"0","AllDay":"1"}'
select
source.DT_RowId as DTRowId_FULL_JSON_Struct /*the full array*/
,JSON_VALUE(source.DT_RowId,'$[0]') AS JSON_VAL_0 /*extract value at index 0 from json structure*/
,JSON_VALUE(source.DT_RowId,'$[1]') AS JSON_VAL_1 /*extract value at index 1 from json structure*/
,JSON_VALUE(source.DT_RowId,'$[2]') AS JSON_VAL_2 /*extract value at index 2 from json structure*/
,source.DT_RowId_Index0 /*already extracted*/
,source.DT_RowId_Index1 /*already extracted*/
,source.DT_RowId_Index2 /*already extracted*/
,source.Schedule
,source.Slot
,source.Period
,source.AllDay
,source.PMOnly
,source.AMOnly
from openjson(#jsonRequest, '$')
with
(
DT_RowId nvarchar(max) as json /*format as json; do the rest in the SELECT statement*/
,DT_RowId_Index0 varchar(2) '$.DT_RowId[0]' /*When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.*/
,DT_RowId_Index1 varchar(2) '$.DT_RowId[1]' /*When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.*/
,DT_RowId_Index2 varchar(2) '$.DT_RowId[2]' /*When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.*/
,Schedule tinyint '$.Schedule'
,Slot tinyint '$.Slot'
,[Period] varchar(20) '$.Period'
,AllDay bit '$.AllDay'
,PMOnly bit '$.PMOnly'
,AMOnly bit '$.AMOnly'
) as source

Supply xml element value in modify() method

I know how to replace element value for the xml element in the modify() method. Here's the example
TSQL Replace value in XML String
My problem is a bit different. Taking example from above link...
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/educational/text())[1]
with sql:variable("#updatedEducation")')
WHERE feedbackID = #FBID
What I want to do is provide value for 'educational'. In other words I want to do something like this
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/sql:variable("#name")/text())[1]
with sql:variable("#value")')
WHERE feedbackID = #FBID
I'm getting the following error because of sql:variable("#name")
The XQuery syntax '/function()' is not supported.
How can I pass both the name of the element to be updated and its value to my
stored procedure and have it update the XML column?
You are not allowed to use variables as part of the XPath, but you can use a predicate:
DECLARE #xml XML=
N'<root>
<optional>
<educational>SomeText</educational>
<someOther>blah</someOther>
</optional>
</root>';
--The straight approach as you know it:
SET #xml.modify('replace value of (/root/optional/educational/text())[1] with "yeah!"');
SELECT #xml;
--Now we use a variable to find the first node below <optional>, which name is as given:
DECLARE #ElementName VARCHAR(100)='educational';
SET #xml.modify('replace value of (/root/optional/*[local-name()=sql:variable("#ElementName")]/text())[1] with "yeah again!"');
SELECT #xml;
Try it out...

Getting multiple values from same xml column in SQL Server

I want to get the values from same xml node under same element.
Sample data:
I have to select all <award_number> values.
This is my SQL code:
DECLARE #xml XML;
DECLARE #filePath varchar(max);
SET #filePath = '<workFlowMeta><fundgroup><funder><award_number>0710564</award_number><award_number>1106058</award_number><award_number>1304977</award_number><award_number>1407404</award_number></funder></fundgroup></workFlowMeta>'
SET #xml = CAST(#filePath AS XML);
SELECT
REPLACE(Element.value('award_number','NVARCHAR(255)'), CHAR(10), '') AS award_num
FROM
#xml.nodes('workFlowMeta/fundgroup/funder') Datalist(Element);
Can't change this #xml.nodes('workFlowMeta/fundgroup/funder'), because I'm getting multiple node values inside funder node.
Can anyone please help me?
Since those <award_number> nodes are inside the <funder> nodes, and there could be several <funder> nodes (if I understood your question correctly), you need to use two .nodes() calls like this:
SELECT
XC.value('.', 'int')
FROM
#xml.nodes('/workFlowMeta/fundgroup/funder') Datalist(Element)
CROSS APPLY
Element.nodes('award_number') AS XT(XC)
The first .nodes() call gets all <funder> elements, and then the second call goes into each <funder> element to get all <award_number> nodes inside of that element and outputs the value of the <award_number> element as a INT (I couldn't quite understand what you're trying to do to the <award_number> value in your code sample....)
Your own code was very close, but
You are diving one level to low
You need to set a singleton XPath for .value(). In most cases this means a [1] at the end)
As you want to read many <award_number> elements, this is the level you have to step down in .nodes(). Reading these element's values is easy, once you have your hands on it:
SELECT
REPLACE(Element.value('text()[1]','NVARCHAR(255)'), CHAR(10), '') AS award_num
FROM
#xml.nodes('/workFlowMeta/fundgroup/funder/award_number') Datalist(Element);
What are you trying to do with the REPLACE()?
If all <arward_number> elements contain valid numbers, you should use int or bigint as target type and there shouldn't be any need to replace non-numeric characters. Try it like this:
SELECT Element.value('text()[1]','int') AS award_num
FROM #xml.nodes('/workFlowMeta/fundgroup/funder/award_number') Datalist(Element);
If marc_s is correct...
... and you have to deal with several <funder> groups, each of which contains several <award_number> nodes, go with his approach (two calls to .nodes())

In SQL Server can I insert multiple nodes into XML from a table?

I want to generate some XML in a stored procedure based on data in a table.
The following insert allows me to add many nodes but they have to be hard-coded or use variables (sql:variable):
SET #MyXml.modify('
insert
<myNode>
{sql:variable("#MyVariable")}
</myNode>
into (/root[1]) ')
So I could loop through each record in my table, put the values I need into variables and execute the above statement.
But is there a way I can do this by just combining with a select statement and avoiding the loop?
Edit I have used SELECT FOR XML to do similar stuff before but I always find it hard to read when working with a hierarchy of data from multiple tables. I was hoping there would be something using the modify where the XML generated is more explicit and more controllable.
Have you tried nesting FOR XML PATH scalar valued functions?
With the nesting technique, you can brake your SQL into very managable/readable elemental pieces
Disclaimer: the following, while adapted from a working example, has not itself been literally tested
Some reference links for the general audience
http://msdn2.microsoft.com/en-us/library/ms178107(SQL.90).aspx
http://msdn2.microsoft.com/en-us/library/ms189885(SQL.90).aspx
The simplest, lowest level nested node example
Consider the following invocation
DECLARE #NestedInput_SpecificDogNameId int
SET #NestedInput_SpecificDogNameId = 99
SELECT [dbo].[udfGetLowestLevelNestedNode_SpecificDogName]
(#NestedInput_SpecificDogNameId)
Let's say had udfGetLowestLevelNestedNode_SpecificDogName had been written without the FOR XML PATH clause, and for #NestedInput_SpecificDogName = 99 it returns the single rowset record:
#SpecificDogNameId DogName
99 Astro
But with the FOR XML PATH clause,
CREATE FUNCTION dbo.udfGetLowestLevelNestedNode_SpecificDogName
(
#NestedInput_SpecificDogNameId
)
RETURNS XML
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar XML
-- Add the T-SQL statements to compute the return value here
SET #ResultVar =
(
SELECT
#SpecificDogNameId as "#SpecificDogNameId",
t.DogName
FROM tblDogs t
FOR XML PATH('Dog')
)
-- Return the result of the function
RETURN #ResultVar
END
the user-defined function produces the following XML (the # signs causes the SpecificDogNameId field to be returned as an attribute)
<Dog SpecificDogNameId=99>Astro</Dog>
Nesting User-defined Functions of XML Type
User-defined functions such as the above udfGetLowestLevelNestedNode_SpecificDogName can be nested to provide a powerful method to produce complex XML.
For example, the function
CREATE FUNCTION [dbo].[udfGetDogCollectionNode]()
RETURNS XML
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar XML
-- Add the T-SQL statements to compute the return value here
SET #ResultVar =
(
SELECT
[dbo].[udfGetLowestLevelNestedNode_SpecificDogName]
(t.SpecificDogNameId)
FROM tblDogs t
FOR XML PATH('DogCollection') ELEMENTS
)
-- Return the result of the function
RETURN #ResultVar
END
when invoked as
SELECT [dbo].[udfGetDogCollectionNode]()
might produce the complex XML node (given the appropriate underlying data)
<DogCollection>
<Dog SpecificDogNameId="88">Dino</Dog>
<Dog SpecificDogNameId="99">Astro</Dog>
</DogCollection>
From here, you could keep working upwards in the nested tree to build as complex an XML structure as you please
CREATE FUNCTION [dbo].[udfGetAnimalCollectionNode]()
RETURNS XML
AS
BEGIN
DECLARE #ResultVar XML
SET #ResultVar =
(
SELECT
dbo.udfGetDogCollectionNode(),
dbo.udfGetCatCollectionNode()
FOR XML PATH('AnimalCollection'), ELEMENTS XSINIL
)
RETURN #ResultVar
END
when invoked as
SELECT [dbo].[udfGetAnimalCollectionNode]()
the udf might produce the more complex XML node (given the appropriate underlying data)
<AnimalCollection>
<DogCollection>
<Dog SpecificDogNameId="88">Dino</Dog>
<Dog SpecificDogNameId="99">Astro</Dog>
</DogCollection>
<CatCollection>
<Cat SpecificCatNameId="11">Sylvester</Cat>
<Cat SpecificCatNameId="22">Tom</Cat>
<Cat SpecificCatNameId="33">Felix</Cat>
</CatCollection>
</AnimalCollection>
Use sql:column instead of sql:variable. You can find detailed info here: http://msdn.microsoft.com/en-us/library/ms191214.aspx
Can you tell a bit more about what exactly you are planning to do.
Is it simply generating XML data based on a content of the table
or adding some data from the table to an existing xml structure?
There are great series of articles on the subject on XML in SQLServer written by Jacob Sebastian, it starts with the basics of generating XML from the data in the table

Using SQL Server 2005's XQuery select all nodes with a specific attribute value, or with that attribute missing

Update: giving a much more thorough example.
The first two solutions offered were right along the lines of what I was trying to say not to do. I can't know location, it needs to be able to look at the whole document tree. So a solution along these lines, with /Books/ specified as the context will not work:
SELECT x.query('.') FROM #xml.nodes('/Books/*[not(#ID) or #ID = 5]') x1(x)
Original question with better example:
Using SQL Server 2005's XQuery implementation I need to select all nodes in an XML document, just once each and keeping their original structure, but only if they are missing a particular attribute, or that attribute has a specific value (passed in by parameter). The query also has to work on the whole XML document (descendant-or-self axis) rather than selecting at a predefined depth.
That is to say, each individual node will appear in the resultant document only if it and every one of its ancestors are missing the attribute, or have the attribute with a single specific value.
For example:
If this were the XML:
DECLARE #Xml XML
SET #Xml =
N'
<Library>
<Novels>
<Novel category="1">Novel1</Novel>
<Novel category="2">Novel2</Novel>
<Novel>Novel3</Novel>
<Novel category="4">Novel4</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
<Volume category="2">G-L</Volume>
<Volume category="3">M-S</Volume>
<Volume category="4">T-Z</Volume>
</Encyclopedia>
</Encyclopedias>
<Dictionaries category="1">
<Dictionary>Webster</Dictionary>
<Dictionary>Oxford</Dictionary>
</Dictionaries>
</Library>
'
A parameter of 1 for category would result in this:
<Library>
<Novels>
<Novel category="1">Novel1</Novel>
<Novel>Novel3</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
</Encyclopedia>
</Encyclopedias>
<Dictionaries category="1">
<Dictionary>Webster</Dictionary>
<Dictionary>Oxford</Dictionary>
</Dictionaries>
</Library>
A parameter of 2 for category would result in this:
<Library>
<Novels>
<Novel category="2">Novel2</Novel>
<Novel>Novel3</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
<Volume category="2">G-L</Volume>
</Encyclopedia>
</Encyclopedias>
</Library>
I know XSLT is perfectly suited for this job, but it's not an option. We have to accomplish this entirely in SQL Server 2005. Any implementations not using XQuery are fine too, as long as it can be done entirely in T-SQL.
It's not clear for me from your example what you're actually trying to achieve. Do you want to return a new XML with all the nodes stripped out except those that fulfill the condition? If yes, then this looks like the job for an XSLT transform which I don't think it's built-in in MSSQL 2005 (can be added as a UDF: http://www.topxml.com/rbnews/SQLXML/re-23872_Performing-XSLT-Transforms-on-XML-Data-Stored-in-SQL-Server-2005.aspx).
If you just need to return the list of nodes then you can use this expression:
//Book[not(#ID) or #ID = 5]
but I get the impression that it's not what you need. It would help if you can provide a clearer example.
Edit: This example is indeed more clear. The best that I could find is this:
SET #Xml.modify('delete(//*[#category!=1])')
SELECT #Xml
The idea is to delete from the XML all the nodes that you don't need, so you remain with the original structure and the needed nodes. I tested with your two examples and it produced the wanted result.
However modify has some restrictions - it seems you can't use it in a select statement, it has to modify data in place. If you need to return such data with a select you could use a temporary table in which to copy the original data and then update that table. Something like this:
INSERT INTO #temp VALUES(#Xml)
UPDATE #temp SET data.modify('delete(//*[#category!=2])')
Hope that helps.
The question is not really clear, but is this what you're looking for?
DECLARE #Xml AS XML
SET #Xml =
N'
<Books>
<Book ID="1">Book1</Book>
<Book ID="2">Book2</Book>
<Book ID="3">Book3</Book>
<Book>Book4</Book>
<Book ID="5">Book5</Book>
<Book ID="6">Book6</Book>
<Book>Book7</Book>
<Book ID="8">Book8</Book>
</Books>
'
DECLARE #BookID AS INT
SET #BookID = 5
DECLARE #Result AS XML
SET #result = (SELECT #xml.query('//Book[not(#ID) or #ID = sql:variable("#BookID")]'))
SELECT #result

Resources