Editing XML From SQL Query Using XML-DML - sql-server

I have an XML column in my MSSQL database whose schema looks similar to this:
<Form>
<Version>1000</Version>
<OtherValues />
</Form>
And I need to manually change (via script) all rows' Version numbers to 1001. Upon searching, I can infer that I'm going to be using the .modify XPath function but all examples I've found have been for inserting nodes, not editing them.
Could someone shed some light on how to do this?

Example data setup:
DECLARE #t TABLE (
Id int
, X xml
)
INSERT #t VALUES ( 1, '
<Form>
<Version>1000</Version>
<OtherValues />
</Form>
'
)
INSERT #t VALUES ( 2, '
<Form>
<Version>1000</Version>
<OtherValues />
</Form>
'
)
Pre-change data:
SELECT * FROM #t
Id X
----------- ------------------------------------------------------------
1 <Form><Version>1000</Version><OtherValues /></Form>
2 <Form><Version>1000</Version><OtherValues /></Form>
Data update:
UPDATE #t
SET X.modify('
replace value of
(/Form/Version[.="1000"]/text())[1]
with
"1001"
')
Post-change data:
SELECT * FROM #t
Id X
----------- ------------------------------------------------------------
1 <Form><Version>1001</Version><OtherValues /></Form>
2 <Form><Version>1001</Version><OtherValues /></Form>
Things to note:
replace value of requires that the 'to-be-replaced' expression identifies a "statical singleton", ie the parser must be able to work out that it refers to a single value - hence the [1]
Only one node (per row) will ever be modified by .modify! So if you have multiple XML nodes in a single row, you will have to iterate manually

Related

Finding a string in XML column using sql server

I have a table with a xml column.
I require to search for sub string in that xml column for all its node and value. Search should be case insensitive
Structure of XML in each row is different
I used below query to do that,
select * from TableName Where Cast(xmlcolumn as varchar(max) ) like '%searchString%'
this works for short length xml rows, if row length goes huge it cant handle the situation. Only partial of the data was searched.
Suggest me some other ways to achieve.
If this is one time task then I would use exist XML method thus:
DECLARE #Table1 TABLE (
ID INT IDENTITY PRIMARY KEY,
CommentAsXML XML
)
INSERT #Table1 (CommentAsXML)
VALUES (N'<root><item /><item type="Reg">0001</item><item type="Inv">B007</item><item type="Cus">A0001</item><item type="Br">F0001</item></root>')
INSERT #Table1 (CommentAsXML)
VALUES (N'<root><item /><item type="Reg">0005</item><parent><child>B007</child></parent><item type="Br">F0005</item></root>')
INSERT #Table1 (CommentAsXML)
VALUES (N'<root><item /><item type="Reg">0005</item></root>')
-- Following query is searching for B007 within InnerText of all XML elements:
SELECT *
FROM #Table1 t
WHERE t.CommentAsXML.exist('//*[lower-case(text()[1]) eq "b007"]') = 1
Results:
ID CommentAsXML
-- ------------------------------------------------------------------------------------------------------------------------------
1 <root><item type="Reg">0001</item><item type="Inv">B007</item><item type="Cus">A0001</item><item type="Br">F0001</item></root>
2 <root><item type="Reg">0005</item><parent><child>B007</child></parent><item type="Br">F0005</item></root>
Also, if you want to search for some text in XML atrributes' values then following XQuery could be used:
SELECT *
FROM #Table1 t
WHERE t.CommentAsXML.exist('//#*[lower-case(.) eq "reg"]') = 1
Note: in both cases, string constants (ex. "reg") should be with lower cases.

Is there a way to add a logical Operator in a WHERE clause using CASE statements? - T-SQL

I searched the web but cannot find a solution for my problem (but perhaps I am using the wrong keywords ;) ).
I've got a Stored Procedure which does some automatic validation (every night) for a bunch of records. However, sometimes a user wants to do the same validation for a single record manually. I thought about calling the Stored Procedure with a parameter, when set the original SELECT statement (which loops through all the records) should get an AND operator with the specified record ID. I want to do it this way so that I don't have to copy the entire select statement and modify it just for the manual part.
The original statement is as follows:
DECLARE GenerateFacturen CURSOR LOCAL FOR
SELECT TOP 100 PERCENT becode, dtreknr, franchisebecode, franchisenemer, fakgroep, vonummer, vovolgnr, count(*) as nrVerOrd,
FaktuurEindeMaand, FaktuurEindeWeek
FROM (
SELECT becode, vonummer, vovolgnr, FaktuurEindeMaand, FaktuurEindeWeek, uitgestfaktuurdat, levdat, voomschrijving, vonetto,
faktureerperorder, dtreknr, franchisebecode, franchisenemer, fakgroep, levscandat
FROM vwOpenVerOrd WHERE becode=#BecondeIN AND levdat IS NOT NULL AND fakstatus = 0
AND isAllFaktuurStukPrijsChecked = 1 AND IsAllFaktuurVrChecked = 1
AND (uitgestfaktuurdat IS NULL OR uitgestfaktuurdat<=#FactuurDate)
) sub
WHERE faktureerperorder = 1
GROUP BY becode, dtreknr, franchisebecode, franchisenemer, fakgroep, vonummer, vovolgnr,
FaktuurEindeMaand, FaktuurEindeWeek
ORDER BY MIN(levscandat)
At the WHERE faktureerperorder = 1 I came up with something like this:
WHERE faktureerperorder = 1 AND CASE WHEN #myParameterManual = 1 THEN vonummer=#vonummer ELSE 1=1 END
But this doesn't work. The #myParameterManual indicates whether or not it should select only a specific record. The vonummer=#vonummer is the record's ID. I thought by setting 1=1 I would get all the records.
Any ideas how to achieve my goal (perhaps more efficient ideas or better ideas)?
I'm finding it difficult to read your query, but this is hopefully a simple example of what you're trying to achieve.
I've used a WHERE clause with an OR operator to give you 2 options on the filter. Using the same query you will get different outputs depending on the filter value:
CREATE TABLE #test ( id INT, val INT );
INSERT INTO #test
( id, val )
VALUES ( 1, 10 ),
( 2, 20 ),
( 3, 30 );
DECLARE #filter INT;
-- null filter returns all rows
SET #filter = NULL;
SELECT *
FROM #test
WHERE ( #filter IS NULL
AND id < 5
)
OR ( #filter IS NOT NULL
AND id = #filter
);
-- filter a specific record
SET #filter = 2;
SELECT *
FROM #test
WHERE ( #filter IS NULL
AND id < 5
)
OR ( #filter IS NOT NULL
AND id = #filter
);
DROP TABLE #test;
First query returns all:
id val
1 10
2 20
3 30
Second query returns a single row:
id val
2 20

How to iterate through a given xml input and insert the data in a TempTable in SQL Server?

Hi I have the following xml input in a SP.
DECLARE #XmlVariable XML = '<portal><patientid>67518</patientid>
<forms>
<form id="31" type="C"/>
<form id="44" type="D"/>
</forms>
</portal>'
I have the following inmemory table:
DECLARE #TColumns table (
FormId int,
FormType varchar(1),
PatientId int
)
Now, my intention is to:
1.Iterate the xml and insert the values into the #TColumns table.
2.Read the #TColumns table row by row and based on the 3 column values update some existing table;something like
update myexistingtable set status=4 where Formid=31 && Formtype='C' and PatientId=67518.
For item number 1 above, this is what I have done till now, but there is some syntax error:
INSERT INTO #TColumns(FormId,FormType,PatientId)
SELECT
XTbl.Cats.value('.', 'int'),
XTbl.Cats.value('.', 'varchar(1)'),
XTbl.Cats.value('.', 'int')
FROM
#XmlVariable.nodes('/portal/forms/form/#id') AS XTbl(Cats),
#XmlVariable.nodes('/portal/forms/form/#type') AS XTbl(Cats),
#XmlVariable.nodes('/portal/forms/form/#patientid') AS XTbl(Cats)
The error I am getting is:The correlation name 'XTbl' is specified multiple times in a FROM clause.
Need help on this and also on the item number 2 above.
Thanks in advance.
Maybe you want something like this
SELECT
Tbl1.Form.value('#id', 'int'),
Tbl1.Form.value('#type', 'varchar(1)'),
Tbl2.Portal.value('patientid', 'int')
FROM
#XmlVariable.nodes('//form') Tbl1(Form),
#XmlVariable.nodes('//portal') Tbl2(Portal)
This is what helped.Yes its is based on Hogan's last suggestion.Thank you!
INSERT INTO #TColumns(FormId,FormType,PatientId)
SELECT
Tbl1.Form.value('#id', 'int'),
Tbl1.Form.value('#type', 'varchar(1)'),
Tbl2.Portal.value('.', 'int')
FROM
#XmlVariable.nodes('//form') Tbl1(Form),
#XmlVariable.nodes('//patientid') Tbl2(Portal)

XQuery adding or replacing attribute in single SQL update command

I have a Table with an XML column,
I want to update the xml to insert attribute or to change the attribute value if the attribute already exists.
Let's say the starting xml is: < d />
Inserting:
UPDATE Table
set XmlCol.modify('insert attribute att {"1"} into /d[1]')
Changing:
UPDATE Table
set XmlCol.modify('replace value of /d[1]/#att with "1"')
insert will fail if the attribute already exists, replace will fail if the attribute doesn't exists.
I have tried to use 'if' but I don't think it can work, there error I get: "XQuery [modify()]: Syntax error near 'attribute', expected 'else'."
IF attempt
UPDATE Table
set XmlCol.modify('if empty(/d[1]/#att)
then insert attribute att {"1"} into /d[1]
else replace value of /d[1]/#att with "1"')
Currently I select the xml into a variable and then modify it using T-SQL and then updating the column with new xml, this requires me to lock the row in a transaction and is probably more expensive for the DB.
From what I can tell, you can't do this with single statement. You can use the exist() method to accomplish that with two update statements.
DECLARE #TestTable TABLE
(
Id int,
XmlCol xml
);
INSERT INTO #TestTable (Id, XmlCol)
VALUES
(1, '<d att="1" />'),
(2, '<d />'),
(3, '<d att="3" />');
SELECT * FROM #TestTable;
UPDATE #TestTable
SET XmlCol.modify('replace value of /d[1]/#att with "1"')
WHERE XmlCol.exist('(/d[1])[not(empty(#att))]') = 1;
UPDATE #TestTable
SET XmlCol.modify('insert attribute att {"1"} into /d[1]')
WHERE XmlCol.exist('(/d[1])[empty(#att)]') = 1;
SELECT * FROM #TestTable;
The output from the final select is:
Id XmlCol
----------- -------------------
1 <d att="1" />
2 <d att="1" />
3 <d att="1" />
There is a slightly better way than Tommys:
DECLARE #TestTable TABLE
(
Id int,
XmlCol xml
);
INSERT INTO #TestTable (Id, XmlCol)
VALUES
(1, '<UserSettings> </UserSettings>'),
(2, '<UserSettings><timeout>3</timeout> </UserSettings>'),
(3, '<UserSettings> </UserSettings>');
UPDATE #TestTable
SET XmlCol.modify('replace value of (/UserSettings/timeout/text())[1] with "1"')
WHERE Id = 3 and XmlCol.exist('/UserSettings/timeout') = 1;
IF ##ROWCOUNT=0
UPDATE #TestTable
SET XmlCol.modify('insert <timeout>5</timeout> into (/UserSettings)[1] ')
WHERE Id = 3;
SELECT * FROM #TestTable;
The solution is a combination of Tommys and simple SQL and required only 1 SQL UPDATE if the column exist. Tommys always recuire two updates.

Numeric comparison of a varchar field in SQL Server

I want to write a query to see if a category field is within a certain range. The problem is the field can contain null, text or numeric text prefixed by '#' character.
Does anybody know of SQL that will strip the non numerics and allow me to do the following check.
category > 1 and category < 100
Here is a sample of what the field category can contain:
#230.1
#200
Null
text
I am using SQL Server 2000
I appears astander's solution is functional. You should consider however a few points:
If the table holds more than a few thousand rows, and if this type of query is to be run frequently, it may be beneficial to introduce a new column to hold the numeric value of the category (if available, null otherwise). This will be more efficient for two reasons: as written, SQL needs to scan the table, completely, i.e.it needs to review every single row; also it needs to perform all these conversion which are a bit expensive, CPU-wise.
You may consider introducing some extra logic to normalize the category field. For example to get rid of common leading or trailing characters etc. This will "rescue" several category codes which would otherwise translate to null wouldn't be able to participate in these filters.
Try something like this
DECLARE #Table TABLE(
Val VARCHAR(200)
)
INSERT INTO #Table (Val) SELECT '#230.1'
INSERT INTO #Table (Val) SELECT '#200'
INSERT INTO #Table (Val) SELECT '210'
INSERT INTO #Table (Val) SELECT NULL
INSERT INTO #Table (Val) SELECT 'text'
SELECT *
FROM (
SELECT CASE
WHEN ISNUMERIC(Val) = 1
THEN CAST(Val AS FLOAT)
WHEN LEN(Val) > 1 AND ISNUMERIC(RIGHT(Val,LEN(Val)-1)) = 1
THEN CAST(RIGHT(Val,LEN(Val)-1) AS FLOAT)
END Num
FROM #Table
WHERE Val IS NOT NULL
AND (
ISNUMERIC(Val) = 1
OR (
LEN(Val) > 1
AND ISNUMERIC(RIGHT(Val,LEN(Val)-1)) = 1
)
)
) Numbers
WHERE Num BETWEEN 205 AND 230

Resources