XPATH Get list item with ID held in sibling - sql-server

Using XPath in T-SQL I am trying to get an attribute value from the options list where the Id to choose the right list item is found in the Value element.
Any help would be appreciated.
declare #myTable table (pk int primary key identity(1,1), myXML xml)
insert into #myTable values ('
<Fields>
<Field ID="1111">
<Description>How Now Brown Cow</Description>
<Value>3</Value>
<Options>
<Options>
<Option OptionContent="Select one" OptionID="-1" />
<Option OptionContent="Mars" OptionID="1" />
<Option OptionContent="Pluto" OptionID="2" />
<Option OptionContent="Saturn" OptionID="3" />
</Options>
</Options>
</Field>
<Field ID="2222">
<Description>Foo Bar</Description>
<Value>2</Value>
<Options>
<Options>
<Option OptionContent="Select one" OptionID="-1" />
<Option OptionContent="Coffee" OptionID="1" />
<Option OptionContent="Tea" OptionID="2" />
<Option OptionContent="Water" OptionID="3" />
<Option OptionContent="Juice" OptionID="4" />
<Option OptionContent="Water" OptionID="5" />
</Options>
</Options>
</Field>
</Fields>
')
select
myField.ref.value('#ID', 'smallint') as [ID]
,myField.ref.value('(./Description)[1]', 'nvarchar(10)') as [Description]
,myField.ref.value('(./Value)[1]', 'int') as [Value]
,myField.ref.value('(./Options/Options/Option[#OptionID="-1"]/#OptionContent)[1]', 'nvarchar(10)') as [SelectedDescription]
from #myTable c
cross apply c.myXML.nodes('/Fields/Field') myField(ref)
ID Description Value Actual Expected
------ ----------- ----------- ---------- --------
1111 How Now Br 3 NULL Saturn
2222 Foo Bar 2 NULL Tea

Change
(./Options/Options/Option[#OptionID="-1"]/#OptionContent)[1]
to
let $id := ./Value[1] return (./Options/Options/Option[#OptionID=$id]/#OptionContent)[1]
So your query should be
select
myField.ref.value('#ID', 'smallint') as [ID]
,myField.ref.value('(./Description)[1]', 'nvarchar(10)') as [Description]
,myField.ref.value('(./Value)[1]', 'int') as [Value]
,myField.ref.value('let $id := ./Value[1] return (./Options/Options/Option[#OptionID=$id]/#OptionContent)[1]', 'nvarchar(10)') as [SelectedDescription]
from #myTable c
cross apply c.myXML.nodes('/Fields/Field') myField(ref)

Related

How can I dynamically change the XML structure of a string in SQL

I need a SQL script that will to pull an XML string from the DB [varchar(max)], inspect it, and update it if it fits a specific situation.
Imagine that my xml is in the following format:
<root>
<level1>
<level2>
<level3 />
<level3 />
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="now is the time for XYZ">
<options>
<option this="that" />
<option me="you" />
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="this one is not of interest">
<options>
<option this="that" />
<option me="you" />
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="now is the time for ABC">
<options>
<option this="that" />
<option me="you" />
<option here="now" />
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
</root>
So, what I want to do is to update all elements whose name is "level6" and which have an attribute called "here" whose value begins with "now is the time". So, that should match just two elements above.
But, that's not the only selection criteria. The list of options must not contain <option here="now" />. So, that should leave us with just one element to update.
<level6 here="now is the time for XYZ">
<options>
<option this="that" />
<option me="you" />
</options>
</level6>
To that element, I then want to add the missing <option here="now" />, so that becomes:
<level6 here="now is the time for XYZ">
<options>
<option this="that" />
<option me="you" />
<option here="now" />
</options>
</level6>
So, the end result should be:
<root>
<level1>
<level2>
<level3 />
<level3 />
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="now is the time for XYZ">
<options>
<option this="that" />
<option me="you" />
<option here="now" /> // <- this one new
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="this one is not of interest">
<options>
<option this="that" />
<option me="you" />
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6 here="now is the time for ABC">
<options>
<option this="that" />
<option me="you" />
<option here="now" />
</options>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
</root>
Assume that I can read the data out of the DB into a string, and that I know how to update the DB, so it's really how to manipulate the xml string in SQL (SQL Server).
You can use XML DML (data modification) with the .modify function to change the XML.
SET #xml.modify('
insert <option here="now" />
as last into
( /root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(/option[#here = "now"])]
)[1]');
This works as follows:
insert <option here="now" /> this is the value we are inserting
as last into it goes after other child nodes of the selected one
/root/level1/level2/level3/level4/level5/level6 this gets us that level6 node
[substring(#here, 1, 15) = "now is the time"] predicates the node to have a here attribute starting with that value. You must modify the length parameter to match the value you are comparing. There is no LIKE in XQuery
/options [not(/option[#here = "now"])] we look for an options node which has no option child which in turn has a here="now" attribute
[1] the first such node
If you need to modify multiple nodes within a single XML document, you need to run this in loop
DECLARE #i int = 20; --max nodes
WHILE #xml.exist('
/root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(option[#here = "now"])]
') = 1
BEGIN
SET #xml.modify('
insert <option here="now" /> as last into
( /root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(option[#here = "now"])]
)[1]');
SET #i -= 1;
IF #i = 0
BREAK;
END;
You can also do this for a whole table
DECLARE #i int = 20; --max nodes
WHILE EXISTS (SELECT 1
FROM YourTable
WHERE XmlColumn.exist('
/root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(option[#here = "now"])]
') = 1)
BEGIN
UPDATE t
SET XmlColumn.modify('
insert <option here="now" /> as last into
( /root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(option[#here = "now"])]
)[1]')
FROM YourTable t
WHERE XmlColumn.exist('
/root/level1/level2/level3/level4/level5/level6
[substring(#here, 1, 15) = "now is the time"]
/options [not(option[#here = "now"])]
') = 1;
SET #i -= 1;
IF #i = 0
BREAK;
END;
For very large datasets it may be faster to rebuild the whole XML using XQuery, with the extra node added using Constructed XML.
db<>fiddle

Bulk import xml file to SQL server

Issue:
I have a single .xml file containing 13.5k of data sets and need to import it into SSMS. Unfortunatly, it contain a style I never met before; first a column declaration part, then the data part without any specific column names. With thus, I have issues to catch the needed fields. On top of that the .xml may even be corrupt (incorrect hierarchical structure).
Xml:
<?xml version="1.0" encoding="UTF-8"?>
<Root>
<DMSContent format="LOL"/>
<Archive name="Adressdossier" id="52" osguid="43AAEC21AC6C40F1BEDB34D92512ED84"/>
<ObjectType name="Dokument" internal_name="CitizenFileDocument" id="262216" osguid="F287C984EB9E48BEA280BA46C305567C" type="DOCUMENT" modul="MULTIDOC"/>
<Rowset>
<Columns>
<Column name="Salutation" type="TEXT" ostype="X" size="50" otype="FOLDER"/>
<Column name="Name" type="TEXT" ostype="X" size="200" otype="FOLDER"/>
<Column name="FirstName" type="TEXT" ostype="X" size="100" otype="FOLDER"/>
<Column name="StreetNo" type="TEXT" ostype="X" size="100" otype="FOLDER"/>
<Column name="City" type="TEXT" ostype="X" size="150" otype="FOLDER"/>
<Column name="ZIP" type="TEXT" ostype="X" size="50" otype="FOLDER"/>
<Column name="Country" type="TEXT" ostype="X" size="50" otype="FOLDER"/>
<Column name="Birthday" type="DATE" ostype="D" size="50" otype="FOLDER"/>
<Column name="Filename" type="INTEGER" ostype="9" size="100" otype="FOLDER"/>
</Columns>
</Rowset>
<Rows>
<Row id="2538">
<Value>Mrs</Value>
<Value>Doe</Value>
<Value>Jane</Value>
<Value>Main Street 5</Value>
<Value>Ghost Town</Value>
<Value>5315</Value>
<Value>Switzerland</Value>
<Value>12.12.2017</Value>
<Value>jp4_B025DF7DBAFC49879103ECB8AE59C3A2.docx</Value>
</Row>
<Row id="2579">
<Value>Mr</Value>
<Value>Ding</Value>
<Value>Chavez</Value>
<Value>Sun Boulevard 3a</Value>
<Value>Alien City</Value>
<Value>4586</Value>
<Value>Germany</Value>
<Value>01.01.1980</Value>
<Value>jp4_DCA9345C93E84F1697668E6ACDC596C9.docx</Value>
</Row>
<Row id="2580">
<Value>Mr</Value>
<Value>Dale</Value>
<Value>Dick</Value>
<Value>Beach Avenue 13</Value>
<Value>Zombie Village</Value>
<Value>9513</Value>
<Value>Italy</Value>
<Value>09.11.1911</Value>
<Value>jp4_5DDBF2A05BD0421A8C53B0CC4EB64232.doc</Value>
</Row>
</Rows>
</Root>
The usually used MS-Sql code snippet, of course not working for this type of .xml-structure:
set ansi_nulls on;
declare #xmlfile xml;
select #xmlfile = bulkcolumn
from openrowset(bulk 'C:\Meta.xml', single_blob) x;
select
id = c.value('#id', 'int'),
Salutation = c.value('(Column[#k="Salutation"]/#v)[1]', 'varchar(60)'),
[Name] = c.value('(Column[#k="name"]/#v)[1]', 'varchar(100)'),
Birthday = c.value('(Column[#k="Birthday"]/#v)[1]', 'date'),
[Filename] = c.value('(Column[#k="Filename"]/#v)[1]', 'varchar(100)')
into #Meta --
from #xmlfile.nodes('/root/rows') as T(c);
set ansi_nulls off;
Thank you in advance for any help!
SQL Server doesn't support fn::position() or preceding-sibling:: syntaxes. But you can use a hack involving << to get the position of each node.
So we calculate the position of each Column node, then push those values into the Value lookups
SELECT
id = x2.Row.value('#id', 'int'),
Salutation = x2.Row.value('(Value[sql:column("ColIndex.Salutation")]/text())[1]', 'varchar(60)'),
[Name] = x2.Row.value('(Value[sql:column("ColIndex.Name" )]/text())[1]', 'varchar(100)'),
Birthday = x2.Row.value('(Value[sql:column("ColIndex.Birthday" )]/text())[1]', 'date'),
[Filename] = x2.Row.value('(Value[sql:column("ColIndex.Filename" )]/text())[1]', 'varchar(100)')
FROM #xml.nodes('/Root/Rowset/Columns') x1(Col)
CROSS APPLY (
SELECT
Salutation = x1.Col.value('let $c:= Column[#name="Salutation"][1] return count(Column[. << $c]) + 1', 'int'),
[Name] = x1.Col.value('let $c:= Column[#name="Name"] [1] return count(Column[. << $c]) + 1', 'int'),
Birthday = x1.Col.value('let $c:= Column[#name="Birthday"] [1] return count(Column[. << $c]) + 1', 'int'),
[Filename] = x1.Col.value('let $c:= Column[#name="Filename"] [1] return count(Column[. << $c]) + 1', 'int')
) ColIndex
CROSS APPLY #xml.nodes('/Root/Rows/Row') x2(Row);
db<>fiddle
If you want to keep your current approach of importing the file, you can, with the following changes:
set ansi_nulls on;
declare #xmlfile xml;
select #xmlfile = bulkcolumn
from openrowset(bulk 'C:\Meta.xml', single_blob) x;
select
id = c.value('#id', 'int'),
Salutation = c.value('(Value[count(/Root/Rowset/Columns/Column[#name="Salutation"]/preceding-sibling::*) + 1]/text())[1]', 'varchar(60)'),
[Name] = c.value('(Value[count(/Root/Rowset/Columns/Column[#name="Name"]/preceding-sibling::*) + 1]/text())[1]', 'varchar(100)'),
Birthday = c.value('(Value[count(/Root/Rowset/Columns/Column[#name="Birthday"]/preceding-sibling::*) + 1]/text())[1]', 'date'),
[Filename] = c.value('(Value[count(/Root/Rowset/Columns/Column[#name="Filename"]/preceding-sibling::*) + 1]/text())[1]', 'varchar(100)')
into #Meta --
from #xmlfile.nodes('/Root/Rows/Row') as T(c);
set ansi_nulls off;
This finds the right <Value> position by looking up the <Column> of the given name and figuring out how many columns precede it. Not pretty, but effective.
If this is a one-off and/or you're certain of the column order, you can of course access the values directly.
Birthday = c.value('(Value[8]/text())[1]', 'varchar(60)'),

How to loop xml in store procedure

I have a store procedure that returns the below XML:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:ReportResponse>
<ns2:responseTitle />
<ns2:responseBody>
<ns2:resultRow>
<ns2:result Name="country" Value="United Kingdom" />
<ns2:result Name="code" Value="7360" />
</ns2:resultRow>
<ns2:resultRow>
<ns2:result Name="country" Value="France" />
<ns2:result Name="code" Value="7340" />
</ns2:resultRow>
</ns2:responseBody>
</ns2:ReportResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I want to be able to save the 2 records in the table, how can I get a loop to get the data?
Record 1:
Country=United Kingdom
Code=7360
Record 2:
Country=France
Code=7340
I tried to use this select but it's not returning anything.
SELECT
Record.value('#Name','VARCHAR')
FROM #XmlResponse.nodes('/Envelope/Body/ReportResponse/responseBody/resultRow')AS TEMPTABLE(Record)
Thanks.
Like this:
declare #doc xml = '
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:ReportResponse xmlns:ns2="http://whatever">
<ns2:responseTitle />
<ns2:responseBody>
<ns2:resultRow>
<ns2:result Name="country" Value="United Kingdom" />
<ns2:result Name="code" Value="7360" />
</ns2:resultRow>
<ns2:resultRow>
<ns2:result Name="country" Value="France" />
<ns2:result Name="code" Value="7340" />
</ns2:resultRow>
</ns2:responseBody>
</ns2:ReportResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
';
WITH XMLNAMESPACES ('http://schemas.xmlsoap.org/soap/envelope/' as soap ,
'http://whatever' as ns2)
SELECT
Record.value('(ns2:result[#Name="country"])[1]/#Value','VARCHAR(20)') Country,
Record.value('(ns2:result[#Name="code"])[1]/#Value','int') Code
FROM #doc.nodes('/soap:Envelope/soap:Body/ns2:ReportResponse/ns2:responseBody/ns2:resultRow')AS TEMPTABLE(Record)
outputs
Country Code
-------------------- -----------
United Kingdom 7360
France 7340
(2 rows affected)

Visual Studio: Updating 2 tables using a grid view

I want to have a gridview which contains Members.firstName, Members.LastName, Team.TeamName. Team.TeamName should be a dropdown containing the possible TeamName's.
My Tables:
Members
--------
MemberID BIGINT
FirstName NVARCHAR(50)
LastName NVARCHAR(50)
TeamID BIGINT
Team
---------
TeamID BIGINT
TeamName NVARCHAR(50)
My Select:
SELECT Members.FirstName, Members.LastName, Team.TeamName
FROM Members
INNER JOIN Team ON Members.TeamId = Team.TeamId
My Update:
CREATE PROCEDURE updateTeamMembers
(
#TeamId BIGINT,
#FirstName NVARCHAR(50),
#LastName NVARCHAR(50),
#TeamName NVARCHAR(50)
)
AS
BEGIN
UPDATE Members SET FirstName=#FirstName, LastName=#LastName WHERE TeamId=#TeamId
UPDATE Team SET TeamName=#TeamName WHERE TeamId=#TeamId
END
RETURN
I believe the error to be with the update procedure, I get this error:
Procedure or function updateTeamMembers has too many arguments specified.
My HTML5:
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConflictDetection="CompareAllValues" ConnectionString="<%$ ConnectionStrings:RegistrationConnectionString %>" DeleteCommand="DELETE FROM [Members] WHERE [MemberId] = #original_MemberId AND (([FirstName] = #original_FirstName) OR ([FirstName] IS NULL AND #original_FirstName IS NULL)) AND (([LastName] = #original_LastName) OR ([LastName] IS NULL AND #original_LastName IS NULL)) AND (([TeamId] = #original_TeamId) OR ([TeamId] IS NULL AND #original_TeamId IS NULL))" InsertCommand="INSERT INTO [Members] ([FirstName], [LastName], [TeamId]) VALUES (#FirstName, #LastName, #TeamId)" OldValuesParameterFormatString="original_{0}" SelectCommand="SELECT Members.FirstName, Members.LastName, Team.TeamName FROM Members INNER JOIN Team ON Members.TeamId = Team.TeamId" UpdateCommand="updateTeamMembers" UpdateCommandType="StoredProcedure">
<DeleteParameters>
<asp:Parameter Name="original_MemberId" Type="Int32" />
<asp:Parameter Name="original_FirstName" Type="String" />
<asp:Parameter Name="original_LastName" Type="String" />
<asp:Parameter Name="original_TeamId" Type="Int64" />
</DeleteParameters>
<InsertParameters>
<asp:Parameter Name="FirstName" Type="String" />
<asp:Parameter Name="LastName" Type="String" />
<asp:Parameter Name="TeamId" Type="Int64" />
</InsertParameters>
<UpdateParameters>
<asp:Parameter Name="TeamId" Type="Int64" />
<asp:Parameter Name="FirstName" Type="String" />
<asp:Parameter Name="LastName" Type="String" />
<asp:Parameter Name="TeamName" Type="String" />
</UpdateParameters>
</asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" BackColor="White" BorderColor="#CCCCCC" BorderStyle="None" BorderWidth="1px" CellPadding="3" DataSourceID="SqlDataSource1" Height="217px" Width="596px">
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="First Name" SortExpression="FirstName" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" SortExpression="LastName" />
<asp:BoundField DataField="TeamName" HeaderText="Team Name" SortExpression="TeamName" />
<asp:CommandField ShowEditButton="True" />
</Columns>
Update command when I configure through datasource (Doesn't update):
UPDATE [Members] SET [FirstName] = #FirstName, [LastName] = #LastName, [TeamId] = #TeamId WHERE [MemberId] = #original_MemberId AND (([FirstName] = #original_FirstName) OR ([FirstName] IS NULL AND #original_FirstName IS NULL)) AND (([LastName] = #original_LastName) OR ([LastName] IS NULL AND #original_LastName IS NULL)) AND (([TeamId] = #original_TeamId) OR ([TeamId] IS NULL AND #original_TeamId IS NULL))
When you execute the stored procedure, you specify the arguments comma separated, something like this:
EXEC updateTeamMembers #ParamValue1, #ParamValue2, #ParamValue3, #ParamValue4, #ParamValue5...
First of all, it's a good practice to specify the arguments as key value pair, where the key is the stored procedure parameter name and the value is the value you want to pass to that parameter. So, the above query will look like this:
EXEC updateTeamMembers
#TeamId = #ParamValue1,
#FirstName = #ParamValue2,
#LastName = #ParamValue3,
#TeamName = #ParamValue4
If you do this, will observe that you don't have a fifth parameter.
To help you more, you should paste the code from VS.

Update the XML nodes based on the node value in SQL Server

In my table I have col 1 ,col 2 ,col 3. The col 3 has the XML stored. I want update the Name,Signedby,userid,title,status,lastmodified nodes based on "Name" node.
XML File:
<SignatureSummary>
<SectionList>
<Section>
<Name>A</Name>
<SignedBy></SignedBy>
<UserId></UserId>
<Title></Title>
<Status></Status>
<LastModifiedOn></LastModifiedOn>
</Section>
<Section>
<Name>B</Name>
<SignedBy />
<UserId />
<Title />
<Status />
<LastModifiedOn />
</Section>
</SectionList>
</SignatureSummary>
Try something like this:
SELECT
Col1, Col2,
Section.value('(Name)[1]', 'VARCHAR(50)') AS 'Name',
Section.value('(SignedBy)[1]', 'VARCHAR(50)') AS 'SignedBy',
Section.value('(UserId)[1]', 'VARCHAR(50)') AS 'UserId',
Section.value('(Title)[1]', 'VARCHAR(50)') AS 'Title',
Section.value('(Status)[1]', 'VARCHAR(50)') AS 'Status',
Section.value('(LastModifiedOn)[1]', 'DATETIME') AS 'Last Modified On'
FROM
dbo.YourTable
CROSS APPLY
Col3.nodes('/SignatureSummary/SectionList/Section') AS Sig(Section)

Resources