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.
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;
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
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
Related
Hi i am getting the string literal error when i am trying to add an attribute to the child node. How can i modify my code in order to add an attribute successfully.
declare #count int=(select mxGraphXML.value('count(/mxGraphModel/root/Cell/#Value )','nvarchar') from TABLE_LIST
where Table_ListID=1234 )
declare #index int=1;
while #index<=#count
begin
declare #Value varchar(100)= #graphxml.value('(/mxGraphModel/root/Cell/#Value )[1]','nvarchar');
SET #graphxml.modify('insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)['+convert(varchar,#index)+']');
end
set #index=#index+1;
end
You're using the addition operator where you should be using the CONCAT function. So
'insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)['+convert(varchar,#index)+']'
is being coerced into a number. Try:
CONCAT('insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)[',convert(varchar,#index),']')
instead.
Adam, you can do it in Microsoft T-SQL like this:
declare #sql nvarchar(max)
set #sql = 'set #myxml.modify(''
insert (
attribute scalableFieldId {sql:variable("#sf_id")},
attribute myTypeId {sql:variable("#my_type_id")}
) into (/VB/Condition/Field[#fieldId=sql:variable("#field_id")
and #fieldCode=sql:variable("#field_code")])['+
cast(#instance as varchar(3))+']'')'
exec sp_executesql
#sql
,N'#myxml xml output, #field_code varchar(20),
#field_id varchar(20), #sf_id int, #my_type_id tinyint'
,#myxml = #myxml output
,#field_code = #field_code
,#field_id = #field_id
,#sf_id = #sf_id
,#my_type_id = #my_type_id
See what I've done here? It's just a clever usage of Dynamic SQL to overcome Microsoft's moronic limitation of "string literal error".
IMPORTANT NOTE: yes, you can MOSTLY do this by using sql:variable() in SOME places BUT good luck trying to use it in the node number qualifier inside the square brackets! You can't do this without Dynamic SQL by design!
The trick is not mine actually, I got the idea from https://www.opinionatedgeek.com/Snaplets/Blog/Form/Item/000299/Read after banging my head against the wall for a while.
Feel free to ask questions if my sample does not work or something is not clear.
I'm migrating mysql database to postgres and ran into a roadblock regarding some basic xml functionality. In MySql I had stored procedures which would replace nodes inside xml document but cannot find any way to do so in postgres.
Here's my stored proc from mysql:
CREATE DEFINER=`root`#`localhost` PROCEDURE `SP_UpdateExamFilesXmlNode`(examFileId int, xPathExpression varchar(128), xmlNode longtext)
BEGIN
DECLARE xmlData longtext;
DECLARE newXmlData longtext;
DECLARE xmlNodeCount int;
SET xmlData = NULL;
SELECT xml_data INTO xmlData FROM sonixhub.exam_files WHERE id = examFileId;
IF xmlData IS NOT NULL THEN
-- check if the node already exists and if it does then simply update it
SET xmlNodeCount = ExtractValue(xmlData, CONCAT('count(',xPathExpression,')'));
IF xmlNodeCount > 0 THEN
SET newXmlData = UpdateXML(xmlData, xPathExpression, xmlNode);
-- if node doesn't exist then we have to add it manually
ELSE
SET newXmlData = REPLACE(xmlData, '</ImageXmlData>', CONCAT(xmlNode, '</ImageXmlData>'));
END IF;
UPDATE sonixhub.exam_files SET xml_data = newXmlData WHERE id = examFileId;
ELSE
-- there is no xml found so create xml from scratch and insert the node
SET xmlData = CONCAT('<ImageXmlData>',xmlNode,'</ImageXmlData>');
UPDATE sonixhub.exam_files SET xml_data = xmlData WHERE id = examFileId;
END IF;
END
Is there any way to replicate this functionality in postgres function instead of moving the logic into the application itself?
EDIT - FOUND A SOLUTION TO MY PROBLEM
I found a solution using mix of postgres xml and string formatting functions.
examFileId is used to find the row to be updated with the xml, change the code with your table info
is the hardcoded root node in my case, but you can change it to whatever you like.
Here's how you call the function:
-- this adds <DicomTags> node to your xml value in the table, if <DicomTags> already exists then it's replaced by the one passed in
select update_exam_files_xml_node(1, '/ImageXmlData/DicomTags', '<DicomTags><DicomTag>xxx</DicomTag></DicomTags>');
-- this adds <Settings> node to your xml value in the table, if <Settings> already exists then it's replaced by the one passed in
select update_exam_files_xml_node(1, '/ImageXmlData/Settings', '<Settings>asdf</Settings>');
CREATE OR REPLACE FUNCTION update_exam_files_xml_node(examFileId int, xPathExpression text, xmlNode text)
RETURNS void AS
$BODY$
DECLARE xmlData xml;
DECLARE newXmlData xml;
DECLARE xmlNodeCount int;
DECLARE replaceTag text;
BEGIN
SELECT xml_data INTO xmlData FROM exam_files WHERE id = examFileId;
IF xml_is_well_formed(xmlNode) = false THEN
PERFORM add_error_log('update_exam_files_xml_node', 'xmlNode is not well formed xml');
RETURN;
END IF;
IF xmlData IS NOT NULL THEN
-- check if the node already exists and if it does then simply update it
IF xmlexists(xPathExpression PASSING BY REF xml(xmlData)) = true THEN
-- get the node name
replaceTag := regexp_replace(xPathExpression, '/.*/', '');
-- replace the existing node with the newly passed in node
newXmlData := xml(regexp_replace(xmlData::text, '<'||replaceTag||'>.*</'||replaceTag||'>', xmlNode));
-- if node doesn't exist then we have to add it manually
ELSE
newXmlData := xml(REPLACE(xmlData::text, '</ImageXmlData>', xmlNode||'</ImageXmlData>'));
END IF;
UPDATE exam_files SET xml_data = newXmlData WHERE id = examFileId;
ELSE
-- there is no xml found so create xml from scratch and insert the node
xmlData := '<ImageXmlData>'||xmlNode||'</ImageXmlData>';
UPDATE exam_files SET xml_data = xmlData WHERE id = examFileId;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Glad you got a solution. To be honest, string formatting functions tend to be a bit difficult to reliably use inside SGML due to issues relating to hierarchies of languages. I.e. regexps have hard limits as to what they can do.
A better solution is likely to be to go a very different direction and write your functions in PL/PerlU or PL/Python, and use existing XML processing capabilities for those languages. This is likely to give you a better and more robust solution.
I have an xml structure as follows
<DifficultyRule xmlns="urn:gjensidige:processguide:201201">
<Id>fc39f423-05c0-4de9-ae46-12fe3c0c279b</Id>
<Code>5595e558-7d10-4767-86dc-5d16f24b8151_Code</Code>
<Author />
<Updated>9/5/2012</Updated>
<Sequence>0</Sequence>
<FromControls>
<Control>
<Code>oiuyui</Code>
<Id>70579cbe-c0b5-4b49-a7b8-6201af388f59</Id>
<FilterValues>
<FilterValue xmlns:p5="urn:gjensidige:processguide:201201" p5:Id="b897f3ac-b40f-4b96-b438-eb156a26457e" p5:Code="e" p5:LookupId="3fa26ce7-4031-4e41-92cb-50d8ce56d262" />
</FilterValues>
</Control>
</FromControls>
<DifficultyCode>Red</DifficultyCode>
</DifficultyRule>
I am trying to delete the FilterValue node in the xpath
/qn:DifficultyRule/qn:FromControls/qn:Control/qn:FilterValues/qn:FilterValue
based on the value of the p5:Id attribute but I am confused as to why nodes are not getting deleted.
Below is the script I'm using:
declare #lookupvalueId varchar(50)
declare #ruleId varchar(50)
set #lookupvalueId = 'b897f3ac-b40f-4b96-b438-eb156a26457e'
set #ruleId = 'fc39f423-05c0-4de9-ae46-12fe3c0c279b'
;WITH XMLNAMESPACES ('urn:gjensidige:processguide:201201' as qn)
update pdr_processdefinitionrule
set PDR_RuleXml.modify('delete (/qn:DifficultyRule/qn:FromControls/qn:Control/qn:FilterValues/qn:FilterValue[#Id=sql:variable("#lookupvalueId")])')
where pdr_guid = #ruleId
What am I missing? Any guidance will be appreciated
Well, since your XML attribute id is the XML namespace with the prefix p5, you must also declare and use that second XML namespace in your code:
declare #lookupvalueId varchar(50)
declare #ruleId varchar(50)
set #lookupvalueId = 'b897f3ac-b40f-4b96-b438-eb156a26457e'
set #ruleId = 'fc39f423-05c0-4de9-ae46-12fe3c0c279b'
;WITH XMLNAMESPACES ('urn:gjensidige:processguide:201201' as qn,
'urn:gjensidige:processguide:201201' as p5)
update
pdr_processdefinitionrule
set
PDR_RuleXml.modify('delete (/qn:DifficultyRule/qn:FromControls/qn:Control/qn:FilterValues/qn:FilterValue[#p5:Id=sql:variable("#lookupvalueId")])')
where
pdr_guid = #ruleId
See that second XML namespace definition for p5? See the use of that XML namespace in the qn:FilterValue[#p5:Id=sql:variable("#lookupvalueId")] expression?
I am just starting with xquery and currently I don't know, why the following is not working. I've created two XML files, they are identically, except that one has namespaces and the other doesn't:
<Envelope>
<Header>
<JustAValue>12345</JustAValue>
</Header>
<Body>
<someBody>
<ValueIWant>12345678910</ValueIWant>
</someBody>
</Body>
</Envelope>
<env:Envelope xmlns:env='http://schemas.xmlsoap.org/soap/envelope/'>
<env:Header xmlns:wsa='http://www.w3.org/2005/08/addressing'>
<JustAValue>12345</JustAValue>
</env:Header>
<env:Body xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' wsu:Id='element-28-1344266615792-223644235'>
<ns1:someBody xmlns='http://someURL.com' xmlns:ns1='http://someURL.com'>
<ValueIWant>12345678910</ValueIWant>
</ns1:someBody>
</env:Body>
</env:Envelope>
Both xml files are in a xml field in the database.
Now I try to get the "ValueIWant" and I do it like this:
DECLARE #xml xml
Select #xml = completexml from MyDB.dbo.Xml where PKxml = 3 -- XML without namespaces
declare #somevalue nvarchar(max)
set #somevalue = #xml.value('(/Envelope/Body/someBody/ValueIWant/text())[1]', 'nvarchar(max)');
select #somevalue -- -> 12345678910
Select #xml = completexml from MyDB.dbo.Xml where PKxml = 2 -- XML with namespaces
set #somevalue = #xml.value('
declare namespace env="http://schemas.xmlsoap.org/soap/envelope/";
declare namespace ns1="http://someURL.com";
(/env:Envelope/env:Body/ns1:someBody/ValueIWant/text())[1]', 'nvarchar(max)');
select #somevalue -- -> NULL
What am I doing wrong here?
I've played around a bit with the XML:
<env:Envelope xmlns:env='http://schemas.xmlsoap.org/soap/envelope/'>
<env:Header xmlns:wsa='http://www.w3.org/2005/08/addressing'>
<JustAValue>12345</JustAValue>
</env:Header>
<env:Body xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' wsu:Id='element-28-1344266615792-223644235'>
<ns1:someBody xmlns='http://someURL.com' xmlns:ns1='http://someURL.com'>
<ValueIWant>12345678910</ValueIWant>
</ns1:someBody>
<BodyValue>BodyValue</BodyValue>
<env:BodyValue>BodyValue</env:BodyValue>
</env:Body>
<env:TestValue1>TestValue</env:TestValue1>
<TestValue2>TestValue2</TestValue2>
</env:Envelope>
I can get all the values: BodyValue, TestValue1 and TestValue2. No problem at all, but as soon as I want to change into the ns1 namespace and get ValueIWant, it just returns NULL.
The ValueIWant element tag name is in the default namespace (http://someURL.com), because this namespace is bound to the empty prefix by its parent element:
<ns1:someBody xmlns='http://someURL.com' ...
However in the query there seems to be no default namespace, so that the name test ValueIWant does not match the ValueIWant tag.
Since it is the same URL that is bound to the ns1 prefix, perhaps prefixing ValueIWant with ns1 in your XPath expression would solve the problem?
Alternatively, you could add a
declare default element namespace "http://someURL.com";
in your query. But using the ns1 prefix should normally work as well, because when comparing names ("QNames"), only the namespace and the local name matter, the prefix is irrelevant.
What would the XQuery look like to check if a node exists, and if it does then run a replace statement, if not then an insert statement?
Here's what I have in mind. I want to store whether or not a user has read an important message in XML. Here's what the data would look like.
<usersettings>
<message haveRead="0" messageId="23" ></message>
<message haveRead="1" messageId="22" ></message>
</usersettings>
Basically this XML tells me that the user has read one message, while the other message still needs to be viewed / read.
I want to combine my insert / replace xquery into one statement. Here's what I had in mind.
UPDATE WebUsers SET UserSettings.modify('
declare default element namespace "http://www.test.com/test";
IF a node exists with the messageId
code to replace node with new update
ELSE
code to insert a new node with the provided variables
')
WHERE Id = #WebUserId
I haven't found a really satisfactory way of doing this, but one of these might work for you. I like the first technique better, but I'm annoyed that I haven't found a more elegant way of doing it.
Make sure the node always exists first
DECLARE #messageID int;
SET #messageID=24;
DECLARE #myDoc xml;
SET #myDoc =
'<usersettings>
<message haveRead="0" messageId="23" >msg</message>
<message haveRead="1" messageId="22" >msg</message>
</usersettings>';
SELECT #myDoc;
SET #myDoc.modify('
insert
if (count(//message[#messageId=sql:variable("#messageID")]) = 0)
then <message haveRead="0">new msg</message>
else()
as last into (/usersettings)[1]
');
SELECT #myDoc;
--now do the rest, safe that the node exists
Switching
DECLARE #myDoc xml;
SET #myDoc =
'<usersettings>
<message haveRead="0" messageId="23" >msg</message>
<message haveRead="1" messageId="22" >msg</message>
</usersettings>';
SELECT #myDoc;
DECLARE #messageID int;
SET #messageID=23;
IF #myDoc.exist('//message[#messageId=sql:variable("#messageID")]') = 1
BEGIN
SET #myDoc.modify('replace value of (//message[#messageId=sql:variable("#messageID")]/text())[1]
with "test"')
END
ELSE
BEGIN
SET #myDoc.modify('insert <message haveRead="0">new msg</message>
into (/usersettings)[1]')
END
SELECT #myDoc;