6

I would like to replace an XML node with a new node. I am trying to make this dynamic so the replacement node name is a variable

DECLARE @xmlSource AS XML = '<Root><Transactions><ReplaceMe>This information should be gone</ReplaceMe></Transactions></Root>'
DECLARE @xmlInsert AS XML = '<NewNode>New Information</NewNode>'
DECLARE @NodeName NVARCHAR(500) = 'ReplaceMe'

The resulting XML should look like:

<Root><Transactions><NewNode>New Information</NewNode></Transactions></Root>
6
  • Please check this one. stackoverflow.com/questions/39028873/… Commented Sep 21, 2016 at 20:43
  • Convert XML to table, manipulate, convert back to XML. Commented Sep 21, 2016 at 20:51
  • 1
    @Dileep, your link points to a XDocument C#-solution. Nothing to do with this... Commented Sep 21, 2016 at 20:58
  • @ajeh The OP tells us, that this should be done dynamically, difficult with table-oriented-approach... Commented Sep 21, 2016 at 20:59
  • The issue is not resolved. Commented Sep 27, 2016 at 17:03

2 Answers 2

3

There is no direct approach to replace a complete node with another one.

But you can delete it and insert the new one:

DECLARE @xmlSource AS XML = '<Root><Transactions><ReplaceMe>This information should be gone</ReplaceMe></Transactions></Root>'
DECLARE @xmlInsert AS XML = '<NewNode>New Information</NewNode>'
DECLARE @NodeName NVARCHAR(500) = 'ReplaceMe'

SET @xmlSource.modify('delete /Root/Transactions/*[local-name(.) eq sql:variable("@NodeName")]');

SELECT @xmlSource; --ReplaceMe is gone...

SET @xmlSource.modify('insert sql:variable("@xmlInsert") into (/Root/Transactions)[1]');

SELECT @xmlSource; 

The result

<Root>
  <Transactions>
    <NewNode>New Information</NewNode>
  </Transactions>
</Root>

UPDATE a generic solution

From your comments I understand, that you have no idea about the XML, just the need to replace one node where you know the name with another node...

This solution is string based (which is super ugly anyway) and has some flaws:

  • If there are several nodes with this name, only the first one will be taken
  • If the node exists multiple times with the same content, it would be replaced in both places
  • If your xml contains CDATA-sections they will be transfered into properly escaped normal XML implicitly. No semantic loss, but this could break structural validations...

This should work even with special characters, as all conversions are from XML to NVARCHAR and back. Escaped characters should stay the same on both sides.

Otherwise one had to use a recursive approach to get the full path to the node and build my first statement dynamically. This was cleaner but more heavy...

DECLARE @xmlSource AS XML = '<Root><Transactions><ReplaceMe>This information should be gone</ReplaceMe></Transactions></Root>'
DECLARE @xmlInsert AS XML = '<NewNode>New Information</NewNode>'
DECLARE @NodeName NVARCHAR(500) = 'ReplaceMe'

SELECT 
CAST( 
REPLACE(CAST(@xmlSource AS NVARCHAR(MAX))
       ,CAST(@xmlSource.query('//*[local-name(.) eq sql:variable("@NodeName")][1]') AS NVARCHAR(MAX))
       ,CAST(@xmlInsert AS NVARCHAR(MAX))
        )
AS XML) 
Sign up to request clarification or add additional context in comments.

3 Comments

Sorry for the late reply. I want to replace every instance of "ReplaceMe" I don't know if the XML will be Root->Transactions-ReplaceMe Or Root->SomeOtherNodeName->AndAnotherNodeName->ReplaceMe
@user1603794 please provide an actual example of your XML and the expected output.
I did in the original post.
1

To replace in place with XML we'll need to insert our new nodes immediately before (or after) the nodes we want to replace.

Start off by getting the number of nodes we want to replace:

DECLARE @numToReplace int = @xmlSource.value('count(//*[local-name(.) eq sql:variable("@NodeName")])', 'int')

Then iterate through each node and flag the node to be deleted (this lets us replace the nodes with a node of the same name).

DECLARE @iterator int = @numToReplace
WHILE @iterator > 0
BEGIN
    SET @xmlSource.modify('insert attribute ToDelete {"delete"} into ((//*[local-name(.) eq sql:variable("@NodeName")])[sql:variable("@iterator")])[1]');
    SET @iterator = @iterator - 1
END

n.b. you need to nest the target query ((*query*)[sql:variable("@numToReplace")])[1], it doesn't like variables in the last node indexer

Then insert the new node before each old node

SET @iterator = @numToReplace
WHILE @iterator > 0
BEGIN
    SET @xmlSource.modify('insert sql:variable("@xmlInsert") before ((//*[local-name(.) eq sql:variable("@NodeName")][@ToDelete="true"])[sql:variable("@iterator")])[1]')
    SET @iterator = @iterator - 1;
END

Then you can just remove all the old nodes

SET @xmlSource.modify('delete (//*[local-name(.) eq sql:variable("@NodeName")][@ToDelete="true"])')

1 Comment

This worked great for me with one alteration you may want to make in your example. When creating the ToDelete attribute you set it's value to be "delete". However, when you later refer to it you are looking for the attribute value to be "true". Changing the original attribute value to be "true" and this seems to work exactly how I needed it too... Thanks!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.