3

Is it possible to use an XML DML statement to rename an element in an untyped XML column?

I am in the process of updating an XML Schema Collection on an XML column and need to patch the existing XML instances by renaming one element before I can apply the latest schema.

As far as I can tell from the docs you can only insert / delete nodes or replace their value.

3 Answers 3

2

As the saying goes, "Where there's a will there's a way"

Here's two methods: the first is to simply replace the previous xml with a new xml constructed from the original with the new element name. In my example I've changed Legs/Leg to Limbs/Limb this could get very complicated for anything but the simplest schema

And secondly, a more appropriate approach of combining insert and delete.

I've combined them into one simple example:

declare @xml as xml = '<animal species="Mouse">
  <legs>
    <leg>Front Right</leg>
    <leg>Front Left</leg>
    <leg>Back Right</leg>
    <leg>Back Left</leg>
  </legs>
</animal>'

set @xml = (select 
     t.c.value('@species', 'varchar(max)') as '@species'
    ,(select
     ti.C.value('.', 'varchar(max)') 
from @Xml.nodes('//animal/legs/leg') ti(c) for xml path('limb'), /* root('limb'), */type) as    limbs   
from @xml.nodes('//*:animal') t(c) for xml path('animal'), type)

select @xml;

while (@xml.exist('/animal/limbs/limb') = 1) begin
    /*insert..*/
    set @xml.modify('
            insert <leg>{/animal/limbs/limb[1]/text()}</leg>
            before (/animal/limbs/limb)[1]
        ');
    /*delete..*/
    set @xml.modify('delete (/animal/limbs/limb)[1]');
end

set @xml.modify('
        insert <legs>{/animal/limbs/leg}</legs>
        before (/animal/limbs)[1]
    ');
set @xml.modify('delete (/animal/limbs)[1]');

select @xml;
2
  • Not exactly what I was looking (hoping) for, but still helpful!
    – Sam
    Commented Dec 21, 2010 at 22:54
  • Just to be clear, the example is turning legs/leg into limbs/limb for the first approach and then from limbs/limb to legs/leg for the second approach. I didn't notice that at first, so was confused when I tried copy/paste/tweaking. The second approach worked great for me.
    – Ecyrb
    Commented Oct 25, 2013 at 19:20
0

During development of SQL Server Unit Test (ssut - see related blog post) I wanted to standardize an xml set coming from a tested object. As I will call the tested object multiple times, each time the set and record names will be the same. For reading ease, I want the record set from the original records to be named similar to <original_record_set><original_record /></original_record_set> and the record set for test records to be named similar to <test_record_set><test_record /></ test_record_set >.

Obviously this is trivial to do if you can modify the call in the tested object as first:

SET @output = (SELECT col1, col2
    FROM   @test_object_result
    FOR xml path ( test_record  '), root( test_record_set '));

and then:

SET @output = (SELECT col1, col2
    FROM   @test_object_result
    FOR xml path (  original_record'), root(  original_record_set '));

However, since I'm calling the SAME object multiple times, and "for xml path" does NOT allow variables in the path('...') and root('...') methods, I had to come up with a different method.

This function accepts an xml tree and builds a new tree, replacing the root node with the value of @relation_name and the name of each record with @tuple_name. The new tree is built with all the attributes of the original, even if there are different numbers per record.

EXCEPTIONS
Obviously this does NOT work with multiple element levels! I have built it specifically to handle a single level attribute based tree as shown in the example below. I may build it out for a multi-level mixed attribute/element tree in the future, but I think that the method to do so becomes obvious now that I've solved the basic problem as below, and will leave that exercise to the reader pending that time.

USE [unit_test];
GO
IF EXISTS  (SELECT * FROM   sys.objects  WHERE  object_id = OBJECT_ID(N'[dbo].[standardize_record_set]')   AND type IN ( N'FN', N'IF', N'TF', N'FS', N'FT' ))
  DROP FUNCTION [dbo].[standardize_record_set];
GO
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
SET nocount ON;
GO
/*
DECLARE
  @relation_name nvarchar(150)= N'standardized_record_set',
  @tuple_name    nvarchar(150)= N'standardized_record',
  @xml           xml,
  @standardized_result xml;

SET @xml='<Root>
    <row id="12" two="now1" three="thr1" four="four1" />
    <row id="232" two="now22" three="thr22" />
    <row id="233" two="now23" three="thr23" threeextra="extraattrinthree" />
    <row id="234" two="now24" three="thr24" fourextra="mealsoin four rwo big mone" />
    <row id="235" two="now25" three="thr25" />
</Root>';

execute @standardized_result =  [dbo].[standardize_record_set] @relation_name=@relation_name, @tuple_name=@tuple_name, @xml=@xml;
select @standardized_result;

*/
CREATE FUNCTION [dbo].[standardize_record_set] (@relation_name nvarchar(150)= N'record_set',
                                                @tuple_name    nvarchar(150)= N'record', @xml  xml )
returns XML
AS
  BEGIN
      DECLARE
        @attribute_index int = 1,
        @attribute_count int = 0,
        @record_set      xml = N'<' + @relation_name + ' />',
        @record_name     nvarchar(50) = @tuple_name,
        @builder         nvarchar(max),
        @record          xml,
        @next_record     xml;
      DECLARE @record_table TABLE (
        record xml );

      INSERT INTO @record_table
      SELECT t.c.query('.') AS record
      FROM   @xml.nodes('/*/*') T(c);

      DECLARE record_table_cursor CURSOR FOR
        SELECT cast([record] AS xml)
        FROM   @record_table

      OPEN record_table_cursor

      FETCH NEXT FROM record_table_cursor INTO @next_record

      WHILE @@FETCH_STATUS = 0
        BEGIN
            SET @attribute_index=1;
            SET @attribute_count = @next_record.query('count(/*[1]/@*)').value('.', 'int');
            SET @builder = N'<' + @record_name + N' ';

            -- build up attribute string
            WHILE @attribute_index <= @attribute_count
              BEGIN
                  SET @builder = @builder + @next_record.value('local-name((/*/@*[sql:variable("@attribute_index")])[1])',
                                                               'varchar(max)') + '="' + @next_record.value('((/*/@*[sql:variable("@attribute_index")])[1])',
                                                                                                           'varchar(max)') + '" ';
                  SET @attribute_index = @attribute_index + 1
              END

            -- build record and add to record_set
            SET @record = @builder + ' />';
            SET @record_set.modify('insert sql:variable("@record") into (/*)[1]');

            FETCH NEXT FROM record_table_cursor INTO @next_record
        END

      CLOSE record_table_cursor;

      DEALLOCATE record_table_cursor;

      RETURN @record_set;
  END;

GO 
0

Yes you can use DML to rename an element by snipping it at the node you want renamed, injecting a new node at that element and then pasting the snipped elements back into the xml at that node. Ive done a SQL fiddle to demo. http://sqlfiddle.com/#!3/dc64d/1 This will change

<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>

into

<animal species="Mouse">
<armsandlegs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</armsandlegs>
</animal>

SqlFiddle looks to have long since broken my solution. From memory ive pasted the basis of my solution below...

DECLARE @XML2 xml
DECLARE @XML3 xml = '<limbs></limbs>'
DECLARE @XML xml = 
'<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>'

SET @XML2 = @XML.query('animal/legs/*')

SET @XML.modify('
insert      
    (sql:variable("@XML3"))
after
    (/animal/legs)[1]
')
SET @XML.modify('
delete (/animal/legs[1])
')
SET @XML.modify('
insert      
    (sql:variable("@XML2"))
as last into
    (/animal/limbs)[1]
')
select @XML
1
  • Include the fiddle in your answer so it stands on its own.
    – J0e3gan
    Commented Jan 28, 2016 at 17:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.