Is there a generic way, in T-SQL, to iterate through an xml variable and get the xml for each entity in turn?
DECLARE #xml xml =
'<Entities>
<Entity key="4" attrib1="abc" attrib2="def" />
<Entity key="18" attrib1="ghi" attrib2="jkl" />
<Entity key="938" attrib1="mno" />
</Entities>'
For example, in the above XML, there are three instances of Entity. I want the XML for each one in turn. I can do this in a hard-coded way, e.g.
SELECT #entityxml = #xml.query('/Entities/Entity[1]')
which will return
<Entity key="4" attrib1="abc" attrib2="def" />
I can then change it to a [2] to get the next one, and so forth.
But, according to both BOL and experimentation, the parameter to #xml.query must be a string literal. In other words, a variable can't be used as the parameter, building it with [1], [2], etc., and getting however many entities there are. It appears we have hard-code the queries, which makes for some ugly code.
I know other methods can be used to get the data in the xml into a table, and then iterate through the rows in the table. I'm specifically asking about getting the XML itself, i.e. getting the equivalent of the above #xml.query, but being able to do so without having to hard-code the array value.
Thanks!
You can use position() to select elements based on index:
DECLARE #xml xml =
'<Entities>
<Entity key="4" attrib1="abc" attrib2="def" />
<Entity key="18" attrib1="ghi" attrib2="jkl" />
<Entity key="938" attrib1="mno" />
</Entities>'
declare #i int;
set #i = 2;
select #xml.query('Entities/Entity[position()=sql:variable("#i")]');
Something like this will do the trick:
DECLARE #xml xml =
'<Entities>
<Entity key="4" attrib1="abc" attrib2="def" />
<Entity key="18" attrib1="ghi" attrib2="jkl" />
<Entity key="938" attrib1="mno" />
</Entities>';
SELECT #xml.query('
for $i in //Entity
return
<Entity>
{ $i/#key }
{ $i/#attrib1 }
{ $i/#attrib2 }
</Entity>
');
Related
Is there a way to make function work like an object?
e.g.: I have an xml typed function like this:
CREATE FUNCTION dbo.CompanyXml(#id int) RETURNS XML
AS
BEGIN
RETURN (SELECT id AS [#id], name AS [#name] FROM Companies WHERE id = #id
FOR XML PATH('CompanyType'),TYPE)
END
When I use the function:
SELECT dbo.CompanyXml(1) AS Supplier,
dbo.CompanyXml(2) AS Client
FOR XML PATH('Document'), TYPE
I get:
<Document>
<Supplier>
<CompanyType id="1" name="Company 1" />
</Supplier>
<Client>
<CompanyType id="2" name="Company 2" />
</Client>
</Document>
but i need:
<Document>
<Supplier id="1" name="Company 1" />
<Client id="2" name="Company 2" />
</Document>
Is there a way to achieve this?
[UPDATE] My solution
(inspired by #Shnugo)
I have used table valued function with FOR XML AUTO
CREATE FUNCTION dbo.CompanyTbl(#id int) RETURNS TABLE
AS SELECT id, name FROM Companies WHERE id = #id
used like this
SELECT (SELECT * FROM dbo.CompanyTbl(1) AS Supplier FOR XML AUTO, TYPE),
(SELECT * FROM dbo.CompanyTbl(2) AS Client FOR XML AUTO, TYPE)
FOR XML PATH('Document'), TYPE
The alias AS Supplier or AS Client is the caption for a specific column in your result set. The function you are calling does not know (and can not know), that its result will be displayed as Supplier or as Client...
Further more the element's name must be a literal using FOR XML PATH().
There are three approaches, I'd pick the last:
You can go with a modified function like Kannan Kandasamys' suggestion, but you will need one section per role and you must hand in the "type" as parameter. More roles will need modifications of the function. Might be difficult in deployed databases...
You could create the xml on string level (something like '<' + #element + id="' + ...) and then use a CAST(... AS XML). Be careful how you deal with special characters in this case!
(My choice): Introduce one separate function for each document role. New roles are new functions, which is easier in most cases
For Nr 3 your code would look like
SELECT dbo.SupplierXml(1) AS [*],
dbo.ClientXml(2) AS [*]
FOR XML PATH('Document'), TYPE;
UPDATE One more approach: FOR XML AUTO
Try this:
CREATE DATABASE TestDB;
GO
USE TestDB;
GO
CREATE TABLE TestTable(id INT,SomeOther VARCHAR(100));
INSERT INTO TestTable VALUES(1,'Some 1'),(2,'Some 2');
SELECT * FROM TestTable FOR XML AUTO;
--The result: You see, that the table name is the element's name:
<TestTable id="1" SomeOther="Some 1" />
<TestTable id="2" SomeOther="Some 2" />
--Nice is, that you can force this name using a table alias:
SELECT * FROM TestTable AS OtherName FOR XML AUTO;
--returns
<OtherName id="1" SomeOther="Some 1" />
<OtherName id="2" SomeOther="Some 2" />
GO
USE master;
GO
DROP DATABASE TestDB;
Now the bad thing is, that - again - the alias must be a literal and cannot be passed in as parameter. It is not inlineable, but you might do something like
DECLARE #cmd VARCHAR(1000)='SELECT * FROM YourTable AS ' + #alias + ' FOR XML AUTO';
EXEC (#cmd);
When it comes to dynamically set column names (same applies to element names), you must use some ugly tricks...
You can change the function as below:
CREATE FUNCTION dbo.CompanyXml1(#id int, #type varchar(15)) RETURNS XML
AS
BEGIN
if #type = 'Supplier'
begin
return(
SELECT id AS [#id], name AS [#name] FROM Companies WHERE id = #id
FOR XML PATH('Supplier'),TYPE )
end
else
begin
return(
SELECT id AS [#id], name AS [#name] FROM Companies WHERE id = #id
FOR XML PATH('Client'),TYPE
)
end
return(null);
END
your query as below:
SELECT dbo.CompanyXml1(1, 'Supplier'),
dbo.CompanyXml1(2, 'Client')
FOR XML PATH('Document'), TYPE
Output ...:
<Document>
<Supplier id="1" name="Company 1" />
<Client id="2" name="Company 2" />
</Document>
I need to remove a Xml element which has the attribute of A with the value of xxxx from the Xml values in a column.
Method 1:
update t set x = x.query('//E[#A != "xxxx"]')
Method 2:
update t set x.modify('delete /E[#A = "xxxx"]')
Which one is better?
Both calls would not do the same:
DECLARE #xml XML=
N'<root>
<test pos="1" a="xxx">test 1</test>
<test pos="2" a="SomeOther">test 2</test>
<test pos="3" a="xxx">test 3</test>
<OtherElement>This is another element</OtherElement>
</root>';
--Try this with either this approach
SET #xml=#xml.query(N'//test[#a!="xxx"]')
--Or try it with this
SET #xml.modify(N'delete //test[#a="xxx"]')
SELECT #xml;
The result of the first is
<test pos="2" a="SomeOther">test 2</test>
While the second returns
<root>
<test pos="2" a="SomeOther">test 2</test>
<OtherElement>This is another element</OtherElement>
</root>
XML is not stored as the text you see. It is stored as a tree structure representing a complex document. To modify this is fairly easy, just kick out some elements. The query()approach has to rebuild the XML and replace the first with an new one. So my clear advise is: Use the modify()approach! If you are really good with XQuery and FLWOR the query() approach is much mightier, but this is another story...
I have imported an xsd, containing 258 elements, into my SQL Server 2012 instance. It is mandatory that all 258 elements are present in the final xml. The issue I am having is that 246 of them will contain default values that are identified in the xsd and I do not how to construct my SQL to populate the xml with the default values.
The following is an example I created that illustrates my issue using a much smaller xsd:
DROP XML SCHEMA COLLECTION TestSchema
GO
CREATE XML SCHEMA COLLECTION TestSchema AS
'<schema xmlns="http://www.w3.org/2001/XMLSchema">
<element name="document">
<complexType>
<sequence>
<element minOccurs="0" name="field1" type="string" default="1" />
<element name="field2" type="int" />
</sequence>
</complexType>
</element>
</schema>'
GO
declare #xml xml(TestSchema) = null
declare #reccount table(recordcount int not null)
insert into #reccount select 32
set #xml =
(
select
recordcount as field2
from
#reccount
for xml
PATH('document')
)
select #xml
The value of #xml is:
<document>
<field2>32</field2>
</document>
Whereas i was expecting
<document>
<field1>1</field1>
<field2>32</field2>
</document>
Any ideas how I can generate the default value of field1?
Thanks in advance.
Lucky for me I came across the answer fooling around with the select statement above. I just needed to add the field 'field1' with a '' for the data.
set #xml =
(
select
'' as field1,
recordcount as field2
from
#reccount
for xml
PATH('document')
)
That did it.
I have a multi-valued field
<arr name="colors">
<str>Blue</str>
<str>Red</str>
<str>Orange</str>
<str>Pink</str>
<str>Violet</str>
</arr>
Filled like this:
<entity name="pub_attributes" query=" SELECT name [description] FROM dbo.Colors">
<field name="colors" column="description" />
</entity>
And I need another field with all the colors but only in one line separated by white spaces like
<str name="Colors_All">Bue Red Orange Pink Violet</str>
How can I do this without accessing the Colors table all over again??
Maybe something like this
<entity name="Properites_all" query="
DECLARE #all VARCHAR(MAX)
SET #all = ''
Select #all = #all + ... from '${pub_attributes.colors}'
UNION
Another SELECT that will add more info than just the colors
">
<field name="colors_all" column="description" />
</entity>
I think, what you looking for is copyfield:
copyfield wiki and also you can take a look here:how to use it
Hope that will help.
I have an XML like this stored in an XML datatype column (will have multiple such rows in table)-
<Root xmlns="http://tempuri.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Elem1 type="T1">
<Name type="string" display="First name">John</Name>
<TimeZone display="Time zone">
<DisplayName type="string" display="Display name">GMT Standard Time</DisplayName>
</TimeZone>
</Elem1>
</Root>
How can I filter based on a node element say (using SQL SERVER 2008 R2) - get all 'Elem1' nodes or get all 'Name' nodes or get all TimeZone nodes ? Something like using local-name() function ?
EDIT - Part Solution -
I got the solution partly (see John's reply below and then run this) -
SELECT C1.query('fn:local-name(.)') AS Nodes FROM [dbo].[MyXmlTable] AS MyXML CROSS APPLY MyXML.MyXmlCol.nodes('//*') AS T ( C1 )
The query above returns all the node elements across the TABLE. Now, I want to say filter upon specific elements and return the element and its value or its attribute value. How to achieve this (by using WHERE clause or any other filter mechanism)?
I'm not sure what result you are looking for but something like this perhaps.
declare #T table(XMLCol xml)
insert into #T values
('<Root xmlns="http://tempuri.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Elem1 type="T1">
<Name type="string" display="First name">John</Name>
<TimeZone display="Time zone">
<DisplayName type="string" display="Display name">GMT Standard Time</DisplayName>
</TimeZone>
</Elem1>
</Root>')
declare #Node varchar(50)
set #Node = 'Elem1'
select N.query('.') as Value
from #T as T
cross apply T.XMLCol.nodes('//*[local-name()=sql:variable("#Node")]') as X(N)
Result:
<p1:Elem1 xmlns:p1="http://tempuri.org" type="T1">
<p1:Name type="string" display="First name">John</p1:Name>
<p1:TimeZone display="Time zone">
<p1:DisplayName type="string" display="Display name">GMT Standard Time</p1:DisplayName>
</p1:TimeZone>
</p1:Elem1>
Edit
If you want the actual value instead of the entire XML you can do like this instead.
declare #Node varchar(50)
set #Node = 'TimeZone'
select N.value('.', 'varchar(100)') as Value
from #T as T
cross apply T.XMLCol.nodes('//*[local-name()=sql:variable("#Node")]') as X(N)
Result:
Value
------------------
GMT Standard Time
You can transform XML into table like here:
declare #XML xml='<Root xmlns="http://tempuri.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Elem1 type="T1">
<Name type="string" display="First name">John</Name>
<TimeZone display="Time zone">
<DisplayName type="string" display="Display name">GMT Standard Time</DisplayName>
</TimeZone>
</Elem1>
</Root> '
;WITH XMLNAMESPACES(DEFAULT 'http://tempuri.org'),
numbers as(
SELECT ROW_NUMBER() OVER(ORDER BY o1.object_id,o2.object_id) Num
FROM sys.objects o1 CROSS JOIN sys.objects o2),
c as(
SELECT
b.value('local-name(.)','nvarchar(1000)') Node_Name,
b.value('./text()[1]','nvarchar(1000)') Node_Value,
b.value('count(#*)','nvarchar(MAX)') AttributeCount,
Num Attribute_Number
FROM
#xml.nodes('Root//*') a(b)
CROSS APPLY Numbers
WHERE Num<=b.value('count(#*)','nvarchar(MAX)')
)
SELECT c.Node_Name,c.node_Value,Attribute_Number,
#XML.query('for $Attr in //*/.[local-name(.)=sql:column("Node_Name")]/#*[sql:column("Attribute_Number")] return local-name($Attr)').value('.','nvarchar(MAX)') Attribute_Name,
#XML.value('data(//*/.[local-name(.)=sql:column("Node_Name")]/#*[sql:column("Attribute_Number")])[1]','nvarchar(1000)') Attribute_Value
FROM c
Result:
Node_Name node_Value Attribute_Number Attribute_Name Attribute_Value
Elem1 NULL 1 type T1
Name John 1 type string
Name John 2 display First name
TimeZone NULL 1 display Time zone
DisplayName GMT Standard Time 1 type string
DisplayName GMT Standard Time 2 display Display name
Later you can query this result to get node/attribute value which do you need.
But it works only in your example, when you have only one node and all names are unique. In multinode XML you should use hierarchical numbering like '1-1-2' or something like this. It is much more complicated and i do not suggest to going this way.
It's not clear to me exactly what your output should look like. However, this should get you started:
create table MyXmlTable (MyXmlCol xml)
insert into MyXmlTable (MyXmlCol) values
(
'
<Root xmlns="http://tempuri.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Elem1 type="T1">
<Name type="string" display="First name">John</Name>
<TimeZone display="Time zone">
<DisplayName type="string" display="Display name">GMT Standard Time</DisplayName>
</TimeZone>
</Elem1>
<Elem1 type="T2">
<Name type="string" display="First name">Fred</Name>
<TimeZone display="Time zone">
<DisplayName type="string" display="Display name">EST Standard Time</DisplayName>
</TimeZone>
</Elem1>
</Root>
');
;WITH XMLNAMESPACES(DEFAULT 'http://tempuri.org')
select MyXmlCol.query('/Root/Elem1/Name')
from MyXmlTable
This queries the XML for the "Name" elements -- you can modify the query depending on what kind of output you want exactly. It's a bit long, but the MSDN article on SQLXML is pretty informative:
http://msdn.microsoft.com/en-us/library/ms345117(v=sql.90).aspx
Hope this helps!
John
Update: you can add a where clause something like this. I'm still not clear on what you want the output to look like, but this will filter out the "Elem1" values:
SELECT C1.query('fn:local-name(.)') AS Nodes
FROM [dbo].[MyXmlTable] AS MyXML
CROSS APPLY MyXML.MyXmlCol.nodes('//*') AS T ( C1 )
WHERE CAST(C1.query('fn:local-name(.)') AS NVARCHAR(32)) <> 'Elem1'
One more update; hopefully this is the answer you are looking for!
Try using a wildcard in the query. I had to use dynamic SQL because the XML query() function will only take string literals for paths (you can use sql:variable("#filter") for values, but I wasn't able to get that working for a path.)
DECLARE #filter nvarchar(20)
SET #filter = '*/Elem1'
DECLARE #sqlCommand nvarchar(1000)
SET #sqlCommand =
';WITH XMLNAMESPACES(DEFAULT ''http://tempuri.org'')
select MyXmlCol.query(''' + #filter + ''')
from MyXmlTable'
print #sqlCommand
EXECUTE sp_executesql #sqlCommand, N'#filter nvarchar(20)', #filter = #filter
This will return the Elem1 XML (and all sub-nodes):
<p1:Elem1 xmlns:p1="http://tempuri.org" type="T1">
<p1:Name type="string" display="First name">John</p1:Name>
<p1:TimeZone display="Time zone">
<p1:DisplayName type="string" display="Display name">GMT Standard Time</p1:DisplayName>
</p1:TimeZone>
</p1:Elem1>
<p2:Elem1 xmlns:p2="http://tempuri.org" type="T2">
<p2:Name type="string" display="First name">Fred</p2:Name>
<p2:TimeZone display="Time zone">
<p2:DisplayName type="string" display="Display name">EST Standard Time</p2:DisplayName>
</p2:TimeZone>
</p2:Elem1>
And if you want to pick out "TimeZone" you would do this:
SET #filter = '*/*/TimeZone'