Extracting attributes from XML column in SQL Server 2012 - sql

I have a column called MetaXML, which has XML stored inside. I only want to extract information relating to the URLpanel tag. How do I access the node/element down to the URLPanel tag. I also want to separate out the Cap, CaptionText, Height, HeightType, ID and URL (all contained in URLPanel). Is that possible?
Note: I've removed the schema info from the first line...
<DASHBOARD>
<COLUMNS>
<COLUMN Width="19" WidthType="Percent">
<PANELS>
<ACTIONLISTPANEL Cap="8fdd05da-c6b9-41c3-9e4d-5b661c3e134a"
CaptionText="My Action List" Height="14" HeightType="Percent"
ID="CL_Front9.2_c0_r0" />
<PROCESSPANEL Cap="11e934f8-1aed-4fd3-ad95-42049343a390" CaptionText="My
Processes" Height="19" HeightType="Percent" ID="CL_Front9.2_c0_r1">
<APPOBJECTS>
<APPOBJECT AppObjectID="CL_WfMyMattersActList" ID="8a690a0f-268c-
4eb0-8536-6ac09605b7f8" Type="Process" />
<APPOBJECT AppObjectID="CL_ModifyTimeNarrative" ID="d2f320e9-2797-
4631-b63a-5bdcb241d633" Type="Process" />
<APPOBJECT AppObjectID="Invoice Inquiry" ID="7749f30c-acdf-4c09-
b841-60026e274284" Type="Dashboard" />
</APPOBJECTS>
</PROCESSPANEL>
<DASHBOARDENTRY Cap="13d4cd47-a3cf-4222-b03b-932bd8ff6850"
CaptionText="Inquiry" DBID="CliMattInquiry2" Height="50"
HeightType="Percent" ID="CL_Front9.2_c0_r2" />
<REPORTPANEL Cap="9596ced2-76d2-4ebb-a7a3-4f2bd2450fde" CaptionText="My
Details" Height="17" HeightType="Percent" ID="CL_Front9.2_c0_r3">
<VIEWS CurrentViewID="CL_TkprStaticUser">
<VIEW Cap="5f99bd4b-1509-4a65-a54c-07bbc444aceb" CaptionText="My
Details" ReportLayoutID="CL_TkprStaticUser" Type="Report" />
</VIEWS>
</REPORTPANEL>
</PANELS>
</COLUMN>
<COLUMN Width="77" WidthType="Percent">
<PANELS>
<URLPANEL Cap="de940bd4-80f8-47f4-b721-76eed63c650f" CaptionText="Fee
Earner Dashboard" Height="100" HeightType="Percent" ID="CL_Front9.2_c1_r0"
Url="http://ccdc1sql119/ReportServer_SQLR_FINDW_PRD1/Pages/ReportViewer.aspx?%2fDWR%2fMIP+Phase+3%2fMIP_Landing_Indiv&rs:Command=Render&rc:Parameters=Collapsed" />
</PANELS>
</COLUMN>
</COLUMNS>
<SHORTLIST />
</DASHBOARD>
The XML above is quite small, but I cannot seem to extract the URLPanel tag. I've created the following SQL to try to extract the URLPanel tag:
DECLARE #DataID UNIQUEIDENTIFIER
SET #DataID = 'AEFFF874-5980-4340-92C1-ED6B292AA667'
BEGIN
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..#TempXML') IS NOT NULL
DROP TABLE #TempXML
DECLARE #X XML
SET #X = (SELECT D.MetaXML AS 'MetaXML'
FROM NxFWKAppObjectData AS D
WHERE NxFWKAppObjectDataID = #DataID)
DECLARE #iX INT
EXEC sp_xml_preparedocument #ix output, #x
CREATE TABLE #TempXML (URLPanel NVARCHAR(MAX))
INSERT INTO #TempXML(URLPanel)
SELECT
URLPanel
FROM
OPENXML(#iX, '//DASHBOARD/COLUMNS/COLUMN/PANELS/URLPANEL', 2)
WITH
(URLPanel NVARCHAR(MAX))
SELECT *
FROM #TempXML AS DT
EXEC sp_xml_removedocument #iX
DROP TABLE #TempXML
END

I would recommend to use the built-in native XQuery support (instead of the legacy OPENXML approach which has its problems with memory leaks etc.).
Given your #X variable - try this:
SELECT
Cap = XC.value('#Cap', 'varchar(50)'),
CaptionText = XC.value('#CaptionText', 'varchar(250)'),
Height = XC.value('#Height', 'int'),
HeightType = XC.value('#HeightType', 'varchar(50)'),
ID = XC.value('#ID', 'varchar(50)'),
Url = XC.value('#Url', 'varchar(500)')
FROM
#X.nodes('/DASHBOARD/COLUMNS/COLUMN/PANELS/URLPANEL') AS XT(XC)
This hopefully should return the bits of data you're looking for.

Related

How to update XML elements with new values in SQL script

I have XML in one of the column in the XYZ table, and I need to update the Amount element with a new value instead of 0.00, and the PolicyReference and AccountReference elements with two different values instead of blank.
For example:
<PolicyReference>7657576567</PolicyReference>
<AccountReference>7657576875</AccountReference>
This is my XML in the column :
<document>
<StatusCode>ACV</StatusCode>
<PaymentMethodDetail>
<EFT>
<AccountNumber>123456789</AccountNumber>
<AccountName>ABCDEFGHIJK</AccountName>
</EFT>
</PaymentMethodDetail>
<PaymentExtendedData>
<CHECK>
<Source>System</Source>
<SourceType>ACH</SourceType>
</CHECK>
</PaymentExtendedData>
<PostMarkDate />
<EntryUserId>1</EntryUserId>
<Amount>0.00</Amount>
<EntryDate />
<PolicyLineOfBusiness>LOL</PolicyLineOfBusiness>
<PolicyReference />
<AccountReference />
<AccountId>2034001793</AccountId>
</document>
This is what I have tried:
UPDATE XYZ
SET XmlPayload.modify('replace value of (//document/PolicyReference/)[1] with "<PolicyReference>275654</PolicyReference>"')
WHERE PaymentSearchId = 18785
I am getting an error:
Msg 9341, Level 16, State 1, Line 4
XQuery [XYZ.XmlPayload.modify()]: Syntax error near ')', expected a step expression
I think this is a good question as it presents an interesting challenge of having an existing element without a text value. This is handled differently than simply adding a new element or replacing the contents of an existing one.
First, though, your provided XML was broken. If that is the XML you're receiving, you have other issues. For instance, in your original question, you had </AccountReference> which is invalid syntax by itself. I corrected this to <AccountReference /> both in your question as well as in my example.
With empty XML elements, you need to call the insert text DML of the XML.modify method.
DECLARE #xml XML =
'<document>
<StatusCode>ACV</StatusCode>
<PaymentMethodDetail>
<EFT>
<AccountNumber>123456789</AccountNumber>
<AccountName>ABCDEFGHIJK</AccountName>
</EFT>
</PaymentMethodDetail>
<PaymentExtendedData>
<CHECK>
<Source>System</Source>
<SourceType>ACH</SourceType>
</CHECK>
</PaymentExtendedData>
<PostMarkDate />
<EntryUserId>1</EntryUserId>
<Amount>0.00</Amount>
<EntryDate />
<PolicyLineOfBusiness>LOL</PolicyLineOfBusiness>
<PolicyReference />
<AccountReference />
<AccountId>2034001793</AccountId>
</document>';
DECLARE
#Amount DECIMAL(18,2) = 99.95,
#AccountReference VARCHAR(20) = '7657576875',
#PolicyReference VARCHAR(20) = '7657576567';
/* Update Amount */
SET #xml.modify('
replace value of (/document/Amount/text())[1]
with sql:variable("#Amount")
');
/* Insert the AccountReference text */
SET #xml.modify('
insert text {sql:variable("#AccountReference")} into (/document/AccountReference[1])[1]
');
/* Insert the PolicyReference text */
SET #xml.modify('
insert text {sql:variable("#PolicyReference")} into (/document/PolicyReference[1])[1]
');
/* Show updated XML */
SELECT #xml;
The updated XML is now:
<document>
<StatusCode>ACV</StatusCode>
<PaymentMethodDetail>
<EFT>
<AccountNumber>123456789</AccountNumber>
<AccountName>ABCDEFGHIJK</AccountName>
</EFT>
</PaymentMethodDetail>
<PaymentExtendedData>
<CHECK>
<Source>System</Source>
<SourceType>ACH</SourceType>
</CHECK>
</PaymentExtendedData>
<PostMarkDate />
<EntryUserId>1</EntryUserId>
<Amount>99.95</Amount>
<EntryDate />
<PolicyLineOfBusiness>LOL</PolicyLineOfBusiness>
<PolicyReference>7657576567</PolicyReference>
<AccountReference>7657576875</AccountReference>
<AccountId>2034001793</AccountId>
</document>
An example of updating a table:
DECLARE #xyz TABLE ( PaymentSearchId INT, XmlPayload XML );
INSERT INTO #xyz VALUES ( 18785,
'<document>
<StatusCode>ACV</StatusCode>
<PaymentMethodDetail>
<EFT>
<AccountNumber>123456789</AccountNumber>
<AccountName>ABCDEFGHIJK</AccountName>
</EFT>
</PaymentMethodDetail>
<PaymentExtendedData>
<CHECK>
<Source>System</Source>
<SourceType>ACH</SourceType>
</CHECK>
</PaymentExtendedData>
<PostMarkDate />
<EntryUserId>1</EntryUserId>
<Amount>0.00</Amount>
<EntryDate />
<PolicyLineOfBusiness>LOL</PolicyLineOfBusiness>
<PolicyReference />
<AccountReference />
<AccountId>2034001793</AccountId>
</document>' );
DECLARE
#PaymentSearchId INT = 18785,
#Amount DECIMAL(18,2) = 99.95,
#AccountReference VARCHAR(20) = '7657576875',
#PolicyReference VARCHAR(20) = '7657576567';
/* Update Amount */
UPDATE #xyz
SET XmlPayload.modify('
replace value of (/document/Amount/text())[1]
with sql:variable("#Amount")
')
WHERE PaymentSearchId = #PaymentSearchId;
/* Insert the AccountReference text */
UPDATE #xyz
SET XmlPayload.modify('
insert text {sql:variable("#AccountReference")} into (/document/AccountReference[1])[1]
')
WHERE PaymentSearchId = #PaymentSearchId;
/* Insert the PolicyReference text */
UPDATE #xyz
SET XmlPayload.modify('
insert text {sql:variable("#PolicyReference")} into (/document/PolicyReference[1])[1]
')
WHERE PaymentSearchId = #PaymentSearchId;
/* Show updated XML */
SELECT XmlPayload FROM #xyz WHERE PaymentSearchId = #PaymentSearchId;

Using SQL to Generate XML

I'm trying to use SQL to generate XML in the format:
<ImportSession>
<Batches>
<Batch>
<BatchFields>
<BatchField Name="Field1" Value="1" />
<BatchField Name="Field2" Value="2" />
<BatchField Name="Field3" Value="3" />
</BatchFields>
<Batch>
<Batches>
</ImportSession>
I'm using SQL Server 2008. I wrote this query:
SELECT
(SELECT
(SELECT
'Col' AS [#Name],
FiscalYear AS [#Value]
FROM [ICEM].[dbo].[ExportedBill]
WHERE ExportedBillID = 1
FOR XML PATH ('BatchField'), TYPE)
FROM [ICEM].[dbo].[ExportedBill]
WHERE ExportedBillID = 1
FOR XML PATH ('BatchFields'), ROOT ('Batch'), TYPE)
FROM
[ICEM].[dbo].[ExportedBill]
WHERE
ExportedBillID = 1
FOR XML PATH ('Batches'), ROOT ('ImportSession')
And this results in:
<ImportSession>
<Batches>
<Batch>
<BatchFields>
<BatchField Name="Col" Value="2015" />
</BatchFields>
</Batch>
</Batches>
</ImportSession>
What I need though is every column should have an entry in BatchField. Also I need the column name to show up in the name. So I should get:
<BatchField Name="FiscalYear" Value="2015" />
<BatchField Name="MeterNumber" Value="123456" />
<BatchField Name="Name" Value="John Smith" />
<BatchField Name="Utility" Value="Electricity" />
So can anyone tell me how I modify my query to get what I need?
EDIT:
I figured it out. I needed a second nested Select. I need one for each column. If they proceeding selects use the same tags as a previous Select then the information is concatanated under the same parent tag
SELECT
(SELECT
(SELECT
'FiscalYear' AS [#Name],
FiscalYear AS [#Value]
FROM [ICEM].[dbo].[ExportedBill]
WHERE ExportedBillID = 1
FOR XML PATH ('BatchField'), TYPE),
(SELECT 'FiscalPeriod' AS [#Name],
FiscalPeriod AS [#Value]
FROM [PEEL_ICEM].[dbo].[ExportedBill]
WHERE ExportedBillID = 1
FOR XML PATH ('BatchField'), TYPE)
FROM [ICEM].[dbo].[ExportedBill]
WHERE ExportedBillID = 1
FOR XML PATH ('BatchFields'), ROOT ('Batch'), TYPE)
FROM
[ICEM].[dbo].[ExportedBill]
WHERE
ExportedBillID = 1
FOR XML PATH ('Batches'), ROOT ('ImportSession')
Thing is though, there will be around 70 columns in this table. Ill brute force it for now, but if anyone knows of a better way to do this please let me know. Cheers
You can create separate child elements by adding a blank column separator. e.g.
DECLARE #T TABLE
( FiscalYear INT,
MeterNumber INT,
Name VARCHAR(255),
Utility VARCHAR(255)
);
INSERT #T VALUES (2015, 123456, 'John Smith', 'Electricity');
SELECT [BatchField/#Name] = 'FiscalYear',
[BatchField/#Value] = FiscalYear,
'',
[BatchField/#Name] = 'MeterNumber',
[BatchField/#Value] = MeterNumber,
'',
[BatchField/#Name] = 'Name',
[BatchField/#Value] = Name,
'',
[BatchField/#Name] = 'Utility',
[BatchField/#Value] = Utility
FROM #T
FOR XML PATH('BatchFields'), ROOT('Batch');
Which gives:
<Batch>
<BatchFields>
<BatchField Name="FiscalYear" Value="2015" />
<BatchField Name="MeterNumber" Value="123456" />
<BatchField Name="Name" Value="John Smith" />
<BatchField Name="Utility" Value="Electricity" />
</BatchFields>
</Batch>

parse xml as table

I have some xml like:
<MyDetails>
<detail key="key1" value="value1" />
<detail key="key2" value="value2" />
<detail key="key3" value="value3" />
<detail key="key4" value="value4" />
</MyDetails>
And I want to be able to parse it in a table format of two columns Key, and Value. How can I create a SQL function to achieve this by specifying a Node path i.e. in this case '/MyDetails/detail' and KeyAttributeName and ValueAttributeName. I created the following function but it gives me the error:
ALTER FUNCTION [dbo].[GetXmlTable] (
#XmlSource XML,
#HierarchyPath NVARCHAR(50) = '',
#SpecificKey NVARCHAR(255) = NULL,
#KeyAttributeName NVARCHAR(50) = 'key',
#ValueAttributeName NVARCHAR(50) = 'value'
)
RETURNS #Table TABLE (
[Key] NVARCHAR(255),
[Value] NVARCHAR(500)
)
AS
BEGIN
DECLARE #KeyAttribute NVARCHAR (50) = '#' + #KeyAttributeName
DECLARE #ValueAttribute NVARCHAR (50) = '#' + #ValueAttributeName
DECLARE #Path NVARCHAR (50) = '/' + #HierarchyPath
INSERT INTO #Table
SELECT XmlElement.Attribute.value(#KeyAttribute, 'nvarchar(255)') AS [Key]
,XmlElement.Attribute.value(#ValueAttribute, 'nvarchar(500)') AS [Value]
FROM #XmlSource.nodes(#Path) AS XmlElement(Attribute)
WHERE #SpecificKey IS NULL OR XmlElement.Attribute.value(#KeyAttribute, 'nvarchar(255)') = #SpecificKey
RETURN
END
GO
Error:
Msg 8172, Level 16, State 1, Procedure GetXmlTable, Line 12 The
argument 1 of the XML data type method "nodes" must be a string
literal.
Looking to call the function like this:
select * from dbo.GetXmlTable(CAST('<MyDetails>
<detail key="key1" value="value1" />
<detail key="key2" value="value2" />
<detail key="key3" value="value3" />
<detail key="key4" value="value4" />
</MyDetails>' as XML), 'MyDetails/detail', default, default, default)
UPDATE---------------
I tried using sql variable syntax but the table returned is blank. Can you please point out what I might be doing wrong:
DECLARE #KeyAttr VARCHAR(50) = N'#' + #KeyAttributeName
DECLARE #ValueAttr VARCHAR(50) = N'#' + #ValueAttributeName
DECLARE #Path VARCHAR(100) = '/' + #HierarchyPath
INSERT INTO #Table
SELECT XmlElement.Attribute.value('(*[local-name() = sql:variable("#KeyAttr")])[1]', 'nvarchar(255)') AS [Key]
,XmlElement.Attribute.value('(*[local-name() = sql:variable("#ValueAttr")][1])', 'nvarchar(500)') AS [Value]
FROM #XmlSource.nodes('(*[local-name() = sql:variable("#Path")])') AS XmlElement(Attribute)
WHERE #SpecificKey IS NULL OR XmlElement.Attribute.value('(*[local-name() = sql:variable("#KeyAttr")])[1]', 'nvarchar(255)') = #SpecificKey
This query will shred your XML into a simple table;
declare #xml xml = '<MyDetails>
<detail key="key1" value="value1" />
<detail key="key2" value="value2" />
<detail key="key3" value="value3" />
<detail key="key4" value="value4" />
</MyDetails>'
select
t.c.value('#key', 'varchar(100)') as [key],
t.c.value('#value', 'varchar(100)') as value
from
#xml.nodes('/MyDetails/detail') as t(c)
The error "The argument 1 of the XML data type method "nodes" must be a string literal." means exactly what it says - you can't pass a variable as the argument to the nodes() method. You can however reference variables in the XQuery passed to Nodes() - see sql:variable() for more info.

The target of 'replace' must be at most one node

I'm trying to modify an XML value but keep getting the message
The target of the replace must be at most one node, found attribute(prefType, xdt:untypedAtomic)
First here is my XML
<preferences>
<categories>
<category id="1" prefType="2">
<subcat id="1" prefType="2" />
<subcat id="2" prefType="2" />
<subcat id="3" prefType="2" />
<subcat id="77" prefType="2" />
</category>
<category id="2" prefType="2">
<subcat id="9" prefType="2" />
<subcat id="10" prefType="2" />
<subcat id="11" prefType="2" />
<subcat id="12" prefType="2" />
<subcat id="13" prefType="2" />
<subcat id="14" prefType="2" />
<subcat id="17" prefType="2" />
<subcat id="78" prefType="2" />
<subcat id="101" prefType="2" />
</category>
<category id="3" prefType="2">
<subcat id="18" prefType="2" />
<subcat id="19" prefType="2" />
<subcat id="20" prefType="2" />
</category>
</categories>
</preferences>
And my code
declare #XMLinput as XML;
declare #custXML as XML;
declare #subcatid as nvarchar(3);
declare #interest as nvarchar(8);
declare #newValue as varchar(1);
declare #cnt as int;
set #XMLinput = '<preferences><categoryId>73</categoryId><interestLevel>POSITIVE</interestLevel></preferences>';
-- get the subcatid and interest level
SET #subcatid = #XMLinput.value('(//preferences/categoryId)[1]','nvarchar(3)');
SET #interest = #XMLinput.value('(//preferences/interestLevel)[1]','nvarchar(20)');
SET #newValue =
CASE #interest
WHEN 'POSITIVE' THEN '1'
WHEN 'NEGATIVE' THEN '3'
ELSE '2'
END;
set #custXML = (select Preferences from Customer_Preferences where custID=11584);
select #custXML.exist('//preferences/categories/category/subcat[#id=sql:variable("#subcatid")]');
if (##ROWCOUNT > 0)
BEGIN TRY
BEGIN TRAN;
set #cnt = CAST(CAST(#custXML.query('count(//preferences/categories/category/subcat[#id=(sql:variable("#subcatid"))])') AS VARCHAR) AS INT);
-- replace the value
UPDATE Customer_Preferences
SET preferences.modify('
replace value of
(//*/subcat[#id=sql:variable("#subcatid")]/#prefType[1])
with sql:variable("#newValue")
')
where CustID = 11584;
COMMIT TRAN;
END TRY
BEGIN CATCH
select XACT_STATE() as 'XACT_STATE', ##TRANCOUNT as '##TRANCOUNT';
if ##TRANCOUNT > 0 ROLLBACK TRANSACTION;
END CATCH
select preferences from Customer_Preferences where custid=11584
SELECT XACT_STATE() as 'XACT_STATE', ##TRANCOUNT AS '##TRANCOUNT'
I've tried removing the sql variables and replacing them with fixed values but still get the same issue. I've also tried removing all the XML subcats except for one and the same error occurs.
After 3 hours of working through this and getting no where, I'd really appreciate your help.
Just as further reference (although you solved your problem already on your own) for others with similar problems: Here is what actually went wrong: The error message already indicates that the replace target can be at most one value (meaning you can replace only one value at a time).
However, (//*/subcat[#id=sql:variable("#subcatid")]/#prefType[1]) yields a sequence of results What is means literally is to take each subcat element and select the first attribute with the name prefType. This actually does not make much sense as an XML element can not have multiple attributes with the same name, so the query would be the same without the [1] predicate.
What you probably wanted to write is: Give me each prefType attribute of each subcat element and return only the first one of the whole result set. That is exactly what your working query is doing: (//*/subcat[#id=sql:variable("#subcatid")]/#prefType)[1]

Modify several xml attributes, based on a list

From a previous post:
SQL Server XML add attribute if non-existent
What I would like to do is be able to modify multiple tags. Below is the code that shows what I would like to do, but cannot, since I get the error: The argument 1 of the XML data type method "exist" must be a string literal. Is there a way to modify the XML using variables, rather than literals?
ALTER FUNCTION [dbo].[ConvertXmlData](#xmlData XML)
RETURNS XML
AS
BEGIN
DECLARE #tags TABLE (
ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
TAG VARCHAR(25)
)
INSERT INTO #tags
SELECT 'xxx' UNION
SELECT 'yyy'
DECLARE #counter INT
DECLARE #count INT
DECLARE #id INT
DECLARE #tag VARCHAR(25)
DECLARE #exist VARCHAR(100)
DECLARE #insert VARCHAR(100)
DECLARE #existX VARCHAR(100)
DECLARE #insertX VARCHAR(100)
SET #exist = 'descendant::{0}[not(#runat)]'
SET #insert = 'insert attribute runat { "server" } into descendant::{0}[not(#runat)][1]'
SET #counter = 1
SELECT #count = COUNT(*) FROM #tags
WHILE #counter <= #count BEGIN
SELECT #tag = TAG FROM #tags WHERE ID = #counter
SET #existX = REPLACE(#existX, '[0]', #tag)
WHILE #xmlData.exist(#existX) = 1 BEGIN
SET #xmlData.modify(REPLACE(#insertX, '[0]', #tag));
END
SET #counter = #counter + 1
END
RETURN #xmlData
END
You can't use a variable as an argument to the xml-functions but you can use variables (and table columns) in the literal expression.
I guess this does what you want. At least it should give you an idea of what you can do.
declare #xmlData xml
set #xmlData =
'<something>
<xxx id="1"/>
<xxx id="2" runat="server" />
<xxx id="3"/>
<yyy id="3" />
<zzz id="1"/>
</something>'
declare #tags table
(
id int identity(1,1) primary key,
tag varchar(25)
)
insert into #tags
select 'xxx' union
select 'yyy'
declare #tag varchar(25)
declare #id int
select top 1
#id = id,
#tag = tag
from #tags
order by id
while ##rowcount > 0
begin
while #xmlData.exist('descendant::*[local-name() = sql:variable("#tag") and not(#runat)]') = 1
begin
set #xmlData.modify('insert attribute runat { "server" } into descendant::*[local-name() = sql:variable("#tag") and not(#runat)][1]');
end
select top 1
#id = id,
#tag = tag
from #tags
where id > #id
order by id
end
select #xmlData
Result:
<something>
<xxx id="1" runat="server" />
<xxx id="2" runat="server" />
<xxx id="3" runat="server" />
<yyy id="3" runat="server" />
<zzz id="1" />
</something>