SQLXML - Search and Query node element? - sql-server

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'

Related

Insert XML child node to SQL table

I've got an XML file like this and I'm working with SQL 2014 SP2
<?xml version='1.0' encoding='UTF-8'?>
<gwl>
<version>123456789</version>
<entities>
<entity id="1" version="123456789">
<name>xxxxx</name>
<listId>0</listId>
<listCode>Oxxx</listCode>
<entityType>08</entityType>
<createdDate>03/03/1993</createdDate>
<lastUpdateDate>05/06/2011</lastUpdateDate>
<source>src</source>
<OriginalSource>o_src</OriginalSource>
<aliases>
<alias category="STRONG" type="Alias">USCJSC</alias>
<alias category="WEAK" type="Alias">'OSKOAO'</alias>
</aliases>
<programs>
<program type="21">prog</program>
</programs>
<sdfs>
<sdf name="OriginalID">9876</sdf>
</sdfs>
<addresses>
<address>
<address1>1141, SYA-KAYA STR.</address1>
<country>RU</country>
<postalCode>1234</postalCode>
</address>
<address>
<address1>90, MARATA UL.</address1>
<country>RU</country>
<postalCode>1919</postalCode>
</address>
</addresses>
<otherIds>
<childId>737606</childId>
<childId>737607</childId>
</otherIds>
</entity>
</entities>
</gwl>
I made a script to insert data from the XML to a SQL table. How can I insert child node into a table? I think I should replicate the row for each new child node but i don't know the best way to proceed.
Here is my SQL code
DECLARE #InputXML XML
SELECT #InputXML = CAST(x AS XML)
FROM OPENROWSET(BULK 'C:\MyFiles\sample.XML', SINGLE_BLOB) AS T(x)
SELECT
product.value('(#id)[1]', 'NVARCHAR(10)') id,
product.value('(#version)[1]', 'NVARCHAR(14)') ID
product.value('(name[1])', 'NVARCHAR(255)') name,
product.value('(listId[1])', 'NVARCHAR(9)')listId,
product.value('(listCode[1])', 'NVARCHAR(10)')listCode,
product.value('(entityType[1])', 'NVARCHAR(2)')entityType,
product.value('(createdDate[1])', 'NVARCHAR(10)')createdDate,
product.value('(lastUpdateDate[1])', 'NVARCHAR(10)')lastUpdateDate,
product.value('(source[1])', 'NVARCHAR(15)')source,
product.value('(OriginalSource[1])', 'NVARCHAR(50)')OriginalSource,
product.value('(aliases[1])', 'NVARCHAR(50)')aliases,
product.value('(programs[1])', 'NVARCHAR(50)')programs,
product.value('(sdfs[1])', 'NVARCHAR(500)')sdfs,
product.value('(addresses[1])', 'NVARCHAR(50)')addresses,
product.value('(otherIDs[1])', 'NVARCHAR(50)')otherIDs
FROM #InputXML.nodes('gwl/entities/entity') AS X(product)
You have a lot of different children here...
Just to show the principles:
DECLARE #xml XML=
N'<gwl>
<version>123456789</version>
<entities>
<entity id="1" version="123456789">
<name>xxxxx</name>
<listId>0</listId>
<listCode>Oxxx</listCode>
<entityType>08</entityType>
<createdDate>03/03/1993</createdDate>
<lastUpdateDate>05/06/2011</lastUpdateDate>
<source>src</source>
<OriginalSource>o_src</OriginalSource>
<aliases>
<alias category="STRONG" type="Alias">USCJSC</alias>
<alias category="WEAK" type="Alias">''OSKOAO''</alias>
</aliases>
<programs>
<program type="21">prog</program>
</programs>
<sdfs>
<sdf name="OriginalID">9876</sdf>
</sdfs>
<addresses>
<address>
<address1>1141, SYA-KAYA STR.</address1>
<country>RU</country>
<postalCode>1234</postalCode>
</address>
<address>
<address1>90, MARATA UL.</address1>
<country>RU</country>
<postalCode>1919</postalCode>
</address>
</addresses>
<otherIds>
<childId>737606</childId>
<childId>737607</childId>
</otherIds>
</entity>
</entities>
</gwl>';
-The query will fetch some values from several places.
--It should be easy to get the rest yourself...
SELECT #xml.value('(/gwl/version/text())[1]','bigint') AS [version]
,A.ent.value('(name/text())[1]','nvarchar(max)') AS [Entity_Name]
,A.ent.value('(listId/text())[1]','int') AS Entity_ListId
--more columns taken from A.ent
,B.als.value('#category','nvarchar(max)') AS Alias_Category
,B.als.value('text()[1]','nvarchar(max)') AS Alias_Content
--similar for programs and sdfs
,E.addr.value('(address1/text())[1]','nvarchar(max)') AS Address_Address1
,E.addr.value('(country/text())[1]','nvarchar(max)') AS Address_Country
--and so on
FROM #xml.nodes('/gwl/entities/entity') A(ent)
OUTER APPLY A.ent.nodes('aliases/alias') B(als)
OUTER APPLY A.ent.nodes('programs/program') C(prg)
OUTER APPLY A.ent.nodes('sdfs/sdf') D(sdfs)
OUTER APPLY A.ent.nodes('addresses/address') E(addr)
OUTER APPLY A.ent.nodes('otherIds/childId') F(ids);
The idea in short:
We read non-repeating values (e.g. version) from the xml variable directly
We use .nodes() to return repeating elements as derived sets.
We can use a cascade of .nodes() to dive deeper into repeating child elements by using a relativ Xpath (no / at the beginning).
You have two approaches:
Read the XML like above into a staging table (simply by adding INTO #tmpTable before FROM) and proceed from there (will need one SELECT ... GROUP BY for each type of child).
Create one SELECT per type of child, using only one of the APPLY lines and shift the data into specific child tables.
I would tend to the first one.
This allows to do some cleaning, generate IDs, check for business rules, before you shift this into the target tables.

Query for xml values in sql server

I have xml field in the below table in Data column
ID | website | Data
Following is the xml field
<Product>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>false</boolean>
</field>
</product>
I need to retrieve all the IsCustomer values in my table.
Following is the code part that I tried so far.
SELECT EMP.ED.value() as EmployeeID
FROM [dbo].[Products]
CROSS APPLY Data.nodes('/Product/Field[#Name="IsCustomer"]/Boolean') as EMP(ED)
Can anyone please help me?
First of all: XML is strictly case sensitive! Your XML is not even valid... The leading <Product> is another element-name then the closing </product>. As you seem to use lower letters in all places, I changed it this way.
Your own query is close, but wrong with some capital letters and you did not use the .value()-function properly (missing paramters).
Try this:
DECLARE #mockup TABLE(ID INT IDENTITY,Descr VARCHAR(100),Data XML);
INSERT INTO #mockup VALUES
('Your Sample','<product>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>false</boolean>
</field>
</product>')
,('Your sample plus another field','<product>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>true</boolean>
</field>
<field name="SomeOther" type="System.Boolean, mscorlib">
<boolean>true</boolean>
</field>
</product>')
,('No "IsCustomer" at all','<product>
<field name="SomeOther" type="System.Boolean, mscorlib">
<boolean>true</boolean>
</field>
</product>')
,('Two of them','<product>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>true</boolean>
</field>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>false</boolean>
</field>
</product>');
SELECT * FROM #mockup;
--Your query returning various variants, one of them should be okay for you:
SELECT m.ID
,m.Descr
,fld.value('(boolean/text())[1]','bit')
FROM #mockup AS m
OUTER APPLY m.Data.nodes('/product/field[#name="IsCustomer"]') AS A(fld);
I already wrote a post about XQuerying which might be helpful to you.
Note: XQuerying is case sensitive and you need to position yourself properly. Take a look at the post i linked, and if it does not help i will update this comment with a solution regarding your query
Update
To answer your question
First and foremost you need to have properly formatted XML, which means tags need to match their case sensitivity, and same rule applies when you are referencing a tag in a XQuery.
So one of the solutions would be :
DECLARE #XML as XML
SET #XML = '<Product>
<field name="IsCustomer" type="System.Boolean, mscorlib">
<boolean>false</boolean>
</field>
</Product>'
SELECT EMP.t.value('boolean[1]','varchar(20)') as EmployeeID
FROM #XML.nodes('/Product/field') as EMP(t)

How to use xml type-valued functions in SQL Server FOR XML

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>

SQL Server 2012 create Xml with default values from xsd

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.

Write specifically structured XML from t-sql query

I would like to be able to write out an XML file in this specific format. I've been reading up on bcp and experimenting with FOR XML but can't seem to achieve what I need here. I would appreciate any help in the right direction. While I know I could refactor the code that would consume this XML output; I'd like to be adventurous here and stick to the task at hand without changing anything.
SQL code for recreating a temp var table dataset
declare #dataset table(Color nvarchar(10), Number int, Code nvarchar(10))
insert into #dataset
select 'Green', 12345, 'US1'
union
select 'Red', 56789, 'US2'
select * from #dataset
And from this query I would like to generate the following XML document.
<?xml version="1.0" encoding="utf-8" ?>
<test>
<collection>
<Case>
<input>
<attribute name="Color">Green</attribute>
<attribute name="Number">12345</attribute>
<attribute name="Code">US1</attribute>
</input>
</Case>
<Case>
<input>
<attribute name="Color">Red</attribute>
<attribute name="Number">56789</attribute>
<attribute name="Code">US2</attribute>
</input>
</Case>
</collection>
</test>
I will defer to you experts to tell me if this is too ridiculous to be accomplished, but I think it "is" possible as I've got a little close so far.
I've been tinkering with this and able to write out XML files.
exec master..xp_cmdshell 'bcp "Query Here" queryout "c:\filename.xml" -c -T'
Thanks SO members!
select (
select (
select 'Color' as [attribute/#name],
Color as [attribute],
null,
'Number' as [attribute/#name],
Number as [attribute],
null,
'Code' as [attribute/#name],
Code as [attribute]
for xml path('input'), type
)
from #dataset
for xml path('Case'), root('collection'), type
)
for xml path('test'), type

Resources