xQuery, getting the value with namespaces? - sql-server-2008

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.

Related

unescape diactrics in \u0 format (json) in ms sql (SQL Server)

I'm getting json file, which I load to Azure SQL databese. This json is direct output from API, so there is nothing I can do with it before loading to DB.
In that file, all Polish diactircs are escaped to "C/C++/Java source code" (based on: http://www.fileformat.info/info/unicode/char/0142/index.htm
So for example:
ł is \u0142
I was trying to find some method to convert (unescape) those to proper Polish letters.
In worse case scenario, I can write function which will replace all combinations
Repalce(Replace(Replace(string,'\u0142',N'ł'),'\u0144',N'ń')))
And so on, making one big, terrible function...
I was looking for some ready functions like there is for URLdecode, which was answered here on stack in many topics, and here: https://www.codeproject.com/Articles/1005508/URL-Decode-in-T-SQL
Using this solution would be possible but I cannot figure out cast/convert with proper collation and types in there, to get result I'm looking for.
So if anyone knows/has function that would make conversion in string for unescaping that \u this would be great, but I will manage to write something on my own if I would get right conversion. For example I tried:
select convert(nvarchar(1), convert(varbinary, 0x0142, 1))
I made assumption that changing \u to 0x will be the answer but it gives some Chinese characters. So this is wrong direction...
Edit:
After googling more I found exactly same question here on stack from #Pasetchnik: Json escape unicode in SQL Server
And it looks this would be the best solution that there is in MS SQL.
Onlty thing I needed to change was using NVARCHAR instead of VARCHAR that is in linked solution:
CREATE FUNCTION dbo.Json_Unicode_Decode(#escapedString nVARCHAR(MAX))
RETURNS nVARCHAR(MAX)
AS
BEGIN
DECLARE #pos INT = 0,
#char nvarCHAR,
#escapeLen TINYINT = 2,
#hexDigits TINYINT = 4
SET #pos = CHARINDEX('\u', #escapedString, #pos)
WHILE #pos > 0
BEGIN
SET #char = NCHAR(CONVERT(varbinary(8), '0x' + SUBSTRING(#escapedString, #pos + #escapeLen, #hexDigits), 1))
SET #escapedString = STUFF(#escapedString, #pos, #escapeLen + #hexDigits, #char)
SET #pos = CHARINDEX('\u', #escapedString, #pos)
END
RETURN #escapedString
END
Instead of nested REPLACE you could use:
DECLARE #string NVARCHAR(MAX)= N'\u0142 \u0144\u0142';
SELECT #string = REPLACE(#string,u, ch)
FROM (VALUES ('\u0142',N'ł'),('\u0144', N'ń')) s(u, ch);
SELECT #string;
DBFiddle Demo

The argument 1 of the XML data type method "modify" must be a string literal

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.

Delete xml node from SQL Server 2008 r2 table column based on attribute value

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?

XQuery if exists conditional insert / replace

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;

SQL Server 2008: Rename an element using XML DML?

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