Update all XML element nodes in an XML field of a SQL table

I wrote an electronic invoicing solution for Dynamics GP a long time ago. The XML invoices of various formats are stored against the invoice number in the database. The column was upgraded to XML type when SQL sever started supporting it in SQL 2005. Although it is possible to cast a varchar into XML data type, it makes it simpler to work with as a native XML data type.

Recently an issue surfaced where the tax amount on the invoice lines needed to be zeroed for a set of invoices, as the XML values had been incorrectly generated. Once the invoices had been corrected (only a dozen), they could be marked for retransmission. But how to quickly and easily update the invoices?

We have been using XML functionality in SQL server for another project recently, so it seemed natural to address the issue with with the inbuilt XML functions of SQL server.

Showing an snippet of the

Documents

The nodes in the document all look like this:

<core:TotalTaxAmount>  <core:MonetaryAmount>7.680core:MonetaryAmount>core:TotalTaxAmount>

Whatever the value is, it needs to be zero after the update.

<core:TotalTaxAmount>  <core:MonetaryAmount>0core:MonetaryAmount>core:TotalTaxAmount>

Modify() function

The following is a good guide to using the XML modify() statement in TSQL

Using the modify() method: replace value of

Using this as a guide I end up with the xPath looking like this

UPDATE [dbo].[CA_SOP_EINVOICE_Output]SETXMLToSend.modify('replace value of (//core:TotalTaxAmount/core:MonetaryAmount[text()!="0"])[1]with ("0")')WHERE SOPNUMBE=@SOPNUMBE ANDSOPTYPE=@SOPTYPE

This gives us a name space issue error due to the :core namespaces

XQuery [dbo.CA_SOP_EINVOICE_Output.XMLToSend.modify()]: The name "core" does not denote a namespace.

This is easily resolved by specifying the namespace with

;WITH XMLNAMESPACES('rrn:org.xcbl:schemas/xcbl/v4_0/core/core.xsd' AS core)

Next we get an error of

The target of 'replace value of' must be a non-metadata attribute or an element with simple typed content

This is due to the schema not defining a type for our node, so just update it as text, by adding text()

(//core:TotalTaxAmount/core:MonetaryAmount[text()!="0"]/text())[1]

Only one replace is allowed at a time, this is a real shame as it makes an otherwise trivial statement into a script. To make it work for this problem, a loop is created that will keep updating the first node it finds until all the nodes have been set to zero. This then gives us the final script to do the task as shown below:

DECLARE @SOPTYPE smallint
DECLARE @SOPNUMBE CHAR(21)

DECLARE @KeepLooping bit

SELECT @SOPTYPE=3, @SOPNUMBE='[Enter Doc Number]'

BEGIN TRANSACTION

-- Declare the namespace we are using before each select/update
;WITH XMLNAMESPACES('rrn:org.xcbl:schemas/xcbl/v4_0/core/core.xsd' AS core) 
SELECT @KeepLooping= XMLToSend.exist('//core:TotalTaxAmount/core:MonetaryAmount!=0')  
FROM [dbo].[CA_SOP_EINVOICE_Output]
WHERE 
SOPNUMBE=@SOPNUMBE 
AND SOPTYPE=@SOPTYPE

WHILE
(@KeepLooping=1)
BEGIN

    -- Do the update on first non-matching node each time until they are all done
    ;WITH XMLNAMESPACES('rrn:org.xcbl:schemas/xcbl/v4_0/core/core.xsd' AS core)
    UPDATE 
    [dbo].[CA_SOP_EINVOICE_Output]
    SET
    XMLToSend.modify('
    replace value of 
    (//core:TotalTaxAmount/core:MonetaryAmount[text()!="0"]/text())[1]
    with ("0")')
    WHERE 
    SOPNUMBE=@SOPNUMBE AND
    SOPTYPE=@SOPTYPE

    ;WITH XMLNAMESPACES('rrn:org.xcbl:schemas/xcbl/v4_0/core/core.xsd' AS core) 
    SELECT @KeepLooping= 
           XMLToSend.exist('//core:TotalTaxAmount/core:MonetaryAmount[text()!="0"]')  
    FROM [dbo].[CA_SOP_EINVOICE_Output]
    WHERE 
    SOPNUMBE=@SOPNUMBE AND 
    SOPTYPE=@SOPTYPE

END
ROLLBACK TRANSACTION

That was another interesting dip into SQL Server support for XML!