XQuery if exists conditional insert / replace - sql-server-2008

What would the XQuery look like to check if a node exists, and if it does then run a replace statement, if not then an insert statement?
Here's what I have in mind. I want to store whether or not a user has read an important message in XML. Here's what the data would look like.
<usersettings>
<message haveRead="0" messageId="23" ></message>
<message haveRead="1" messageId="22" ></message>
</usersettings>
Basically this XML tells me that the user has read one message, while the other message still needs to be viewed / read.
I want to combine my insert / replace xquery into one statement. Here's what I had in mind.
UPDATE WebUsers SET UserSettings.modify('
declare default element namespace "http://www.test.com/test";
IF a node exists with the messageId
code to replace node with new update
ELSE
code to insert a new node with the provided variables
')
WHERE Id = #WebUserId

I haven't found a really satisfactory way of doing this, but one of these might work for you. I like the first technique better, but I'm annoyed that I haven't found a more elegant way of doing it.
Make sure the node always exists first
DECLARE #messageID int;
SET #messageID=24;
DECLARE #myDoc xml;
SET #myDoc =
'<usersettings>
<message haveRead="0" messageId="23" >msg</message>
<message haveRead="1" messageId="22" >msg</message>
</usersettings>';
SELECT #myDoc;
SET #myDoc.modify('
insert
if (count(//message[#messageId=sql:variable("#messageID")]) = 0)
then <message haveRead="0">new msg</message>
else()
as last into (/usersettings)[1]
');
SELECT #myDoc;
--now do the rest, safe that the node exists
Switching
DECLARE #myDoc xml;
SET #myDoc =
'<usersettings>
<message haveRead="0" messageId="23" >msg</message>
<message haveRead="1" messageId="22" >msg</message>
</usersettings>';
SELECT #myDoc;
DECLARE #messageID int;
SET #messageID=23;
IF #myDoc.exist('//message[#messageId=sql:variable("#messageID")]') = 1
BEGIN
SET #myDoc.modify('replace value of (//message[#messageId=sql:variable("#messageID")]/text())[1]
with "test"')
END
ELSE
BEGIN
SET #myDoc.modify('insert <message haveRead="0">new msg</message>
into (/usersettings)[1]')
END
SELECT #myDoc;

Related

MYSQL Stored Procedure to Iterate Json Array Data

I have a couple of columns that are json arrays that have datetime data like this:
["2017-04-18 11:05:00.000000"]
["2017-04-20 11:05:00.000000"]
["2017-04-22 11:05:00.000000"]
["2017-12-11 22:14:02.000000", "2017-12-11 22:14:08.000000", "2017-12-11 22:19:13.000000", "2017-12-11 22:20:44.000000", "2017-12-11 22:21:54.000000", "2017-12-11 22:23:09.000000"]
["2017-12-13 13:21:04.000000"]
["2017-12-14 13:10:44.000000", "2017-12-14 13:21:51.000000"]
["2017-12-15 13:27:21.000000", "2017-12-15 13:30:21.000000"]
["2017-12-16 15:15:22.000000"]
The goal is to parse out the datetime data and store it into a separate table from which I plan on doing some fun stuff. Currently, it only inserts the first record only, and it inserts it ~180000 times. My current code is:
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE i INTEGER DEFAULT 0;
DECLARE usages VARCHAR(4000);
-- declare cursor for employee email
DEClARE curUsages
CURSOR FOR
SELECT associated_usages from usagesTbl where associated_usages not like '[]';
-- declare NOT FOUND handler
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET finished = 1;
OPEN curUsages;
getUsages: LOOP
FETCH curUsages INTO usages;
IF finished = 1 THEN
LEAVE getUsages;
END IF;
WHILE i < JSON_LENGTH(usages) DO
INSERT INTO usagesTbl VALUES (JSON_EXTRACT(usages, CONCAT('$[',i,']')));
SET i = i + 1;
END WHILE;
SET i = 0;
END LOOP getUsages;
CLOSE curUsages;
END;
it seems that the while loop variable "i" is not increasing, and I am getting constantly stuck in the loop. The reason for me thinking this is that I pulled out the JSON_EXTRACT code and wrote this for testing:
set #i = 0;
select JSON_EXTRACT(associated_usages, CONCAT('$[',#i,']')) from usagesTbl where associated_usages not like '[]';
I can change the value of #i to whatever index I want and I get the right data. Im just stuck on why it doesn't work in the while loop during the stored procedure. Any help is greatly appreciated!
not sure if this could be the issue, but I see this:
DEClARE curUsages
Should be this:
DECLARE curUsages
Can it be the simple typo ? (the 1 for the L)
Fixed it! It somehow created an infinite loop that just kept on inserting data even when the stored proc said it was done running. I dropped and recreated the table, and changed the datatype of usages back from VARCHAR to json, and it worked like a charm.

The argument 1 of the XML data type method "modify" must be a string literal

Hi i am getting the string literal error when i am trying to add an attribute to the child node. How can i modify my code in order to add an attribute successfully.
declare #count int=(select mxGraphXML.value('count(/mxGraphModel/root/Cell/#Value )','nvarchar') from TABLE_LIST
where Table_ListID=1234 )
declare #index int=1;
while #index<=#count
begin
declare #Value varchar(100)= #graphxml.value('(/mxGraphModel/root/Cell/#Value )[1]','nvarchar');
SET #graphxml.modify('insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)['+convert(varchar,#index)+']');
end
set #index=#index+1;
end
You're using the addition operator where you should be using the CONCAT function. So
'insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)['+convert(varchar,#index)+']'
is being coerced into a number. Try:
CONCAT('insert attribute copyValueID {sql:variable("#Value ")}
as first into (/mxGraphModel/root/Cell)[',convert(varchar,#index),']')
instead.
Adam, you can do it in Microsoft T-SQL like this:
declare #sql nvarchar(max)
set #sql = 'set #myxml.modify(''
insert (
attribute scalableFieldId {sql:variable("#sf_id")},
attribute myTypeId {sql:variable("#my_type_id")}
) into (/VB/Condition/Field[#fieldId=sql:variable("#field_id")
and #fieldCode=sql:variable("#field_code")])['+
cast(#instance as varchar(3))+']'')'
exec sp_executesql
#sql
,N'#myxml xml output, #field_code varchar(20),
#field_id varchar(20), #sf_id int, #my_type_id tinyint'
,#myxml = #myxml output
,#field_code = #field_code
,#field_id = #field_id
,#sf_id = #sf_id
,#my_type_id = #my_type_id
See what I've done here? It's just a clever usage of Dynamic SQL to overcome Microsoft's moronic limitation of "string literal error".
IMPORTANT NOTE: yes, you can MOSTLY do this by using sql:variable() in SOME places BUT good luck trying to use it in the node number qualifier inside the square brackets! You can't do this without Dynamic SQL by design!
The trick is not mine actually, I got the idea from https://www.opinionatedgeek.com/Snaplets/Blog/Form/Item/000299/Read after banging my head against the wall for a while.
Feel free to ask questions if my sample does not work or something is not clear.

Case Insensitive REPLACE for MySQL

Is there a case insensitive Replace for MySQL?
I'm trying to replace a user's old username with their new one within a paragraph text.
$targetuserold = "#".$mynewusername;
$targetusernew = "#".$newusername;
$sql = "
UPDATE timeline
SET message = Replace(message,'".$targetuserold."', '".$targetusernew."')
";
$result = mysql_query($sql);
This is missing the instances where the old username is a different case. Example: replacing "Hank" with "Jack" in all the rows in my database will leave behind instances of "hank".
An easier way that works without any stored function:
SELECT message,
substring(comments,position(lower('".$targetuserold."') in message) ) AS oldval
FROM timeline
WHERE message LIKE '%".$targetuserold."%'
gives you the exact, case sensitive spellings of the username in all messages. As you seem to run that from a PHP script, you could use that to collect the spellings together with the corresponding IDs, and then run a simple REPLACE(message,'".$oldval.",'".$targetusernew."') on that. Or use the above as sub-select:
UPDATE timeline
SET message = REPLACE(
message,
(SELECT substring(comments,position(lower('".$targetuserold."') in message))),
'".$targetusernew."'
)
Works like a charm here.
Credits given to this article, where I got the idea from.
Here it is:
DELIMITER $$
DROP FUNCTION IF EXISTS `replace_ci`$$
CREATE FUNCTION `replace_ci` ( str TEXT,needle CHAR(255),str_rep CHAR(255))
RETURNS TEXT
DETERMINISTIC
BEGIN
DECLARE return_str TEXT DEFAULT '';
DECLARE lower_str TEXT;
DECLARE lower_needle TEXT;
DECLARE pos INT DEFAULT 1;
DECLARE old_pos INT DEFAULT 1;
SELECT lower(str) INTO lower_str;
SELECT lower(needle) INTO lower_needle;
SELECT locate(lower_needle, lower_str, pos) INTO pos;
WHILE pos > 0 DO
SELECT concat(return_str, substr(str, old_pos, pos-old_pos), str_rep) INTO return_str;
SELECT pos + char_length(needle) INTO pos;
SELECT pos INTO old_pos;
SELECT locate(lower_needle, lower_str, pos) INTO pos;
END WHILE;
SELECT concat(return_str, substr(str, old_pos, char_length(str))) INTO return_str;
RETURN return_str;
END$$
DELIMITER ;
Usage:
$sql = "
UPDATE timeline
SET message = replace_ci(message,'".$targetuserold."', '".$targetusernew."')
";
My solution ultimately was that I cannot do a case insensitive Replace.
However, I did find a workaround.
I was trying to have a feature where a user can change their username. The system would then need to update wherever #oldusername was found in all the messages in the database.
The problem was... people wouldn't type other people's usernames in the correct case that it is found in the members table. So when the user would change their username, it wouldn't catch those instances of #oldSeRNAmE because of it not matching the case of the real format of the oldusername.
I don't have permission with my GoDaddy shared server to do this with a customized SQL function, so I had to find a different way.
My solution: Upon inserting new messages into the database, whenever a username is found in the new message, I have an UPDATE statement at that point to replace the username they typed with the correct formatted case that is found in the members table. That way, if that person ever wants to change their username in the future, all the instances of that username in the database will all be the same exact formatted case. Problem solved.

postgres - modifying xml (MySql UpdateXml alternative)

I'm migrating mysql database to postgres and ran into a roadblock regarding some basic xml functionality. In MySql I had stored procedures which would replace nodes inside xml document but cannot find any way to do so in postgres.
Here's my stored proc from mysql:
CREATE DEFINER=`root`#`localhost` PROCEDURE `SP_UpdateExamFilesXmlNode`(examFileId int, xPathExpression varchar(128), xmlNode longtext)
BEGIN
DECLARE xmlData longtext;
DECLARE newXmlData longtext;
DECLARE xmlNodeCount int;
SET xmlData = NULL;
SELECT xml_data INTO xmlData FROM sonixhub.exam_files WHERE id = examFileId;
IF xmlData IS NOT NULL THEN
-- check if the node already exists and if it does then simply update it
SET xmlNodeCount = ExtractValue(xmlData, CONCAT('count(',xPathExpression,')'));
IF xmlNodeCount > 0 THEN
SET newXmlData = UpdateXML(xmlData, xPathExpression, xmlNode);
-- if node doesn't exist then we have to add it manually
ELSE
SET newXmlData = REPLACE(xmlData, '</ImageXmlData>', CONCAT(xmlNode, '</ImageXmlData>'));
END IF;
UPDATE sonixhub.exam_files SET xml_data = newXmlData WHERE id = examFileId;
ELSE
-- there is no xml found so create xml from scratch and insert the node
SET xmlData = CONCAT('<ImageXmlData>',xmlNode,'</ImageXmlData>');
UPDATE sonixhub.exam_files SET xml_data = xmlData WHERE id = examFileId;
END IF;
END
Is there any way to replicate this functionality in postgres function instead of moving the logic into the application itself?
EDIT - FOUND A SOLUTION TO MY PROBLEM
I found a solution using mix of postgres xml and string formatting functions.
examFileId is used to find the row to be updated with the xml, change the code with your table info
is the hardcoded root node in my case, but you can change it to whatever you like.
Here's how you call the function:
-- this adds <DicomTags> node to your xml value in the table, if <DicomTags> already exists then it's replaced by the one passed in
select update_exam_files_xml_node(1, '/ImageXmlData/DicomTags', '<DicomTags><DicomTag>xxx</DicomTag></DicomTags>');
-- this adds <Settings> node to your xml value in the table, if <Settings> already exists then it's replaced by the one passed in
select update_exam_files_xml_node(1, '/ImageXmlData/Settings', '<Settings>asdf</Settings>');
CREATE OR REPLACE FUNCTION update_exam_files_xml_node(examFileId int, xPathExpression text, xmlNode text)
RETURNS void AS
$BODY$
DECLARE xmlData xml;
DECLARE newXmlData xml;
DECLARE xmlNodeCount int;
DECLARE replaceTag text;
BEGIN
SELECT xml_data INTO xmlData FROM exam_files WHERE id = examFileId;
IF xml_is_well_formed(xmlNode) = false THEN
PERFORM add_error_log('update_exam_files_xml_node', 'xmlNode is not well formed xml');
RETURN;
END IF;
IF xmlData IS NOT NULL THEN
-- check if the node already exists and if it does then simply update it
IF xmlexists(xPathExpression PASSING BY REF xml(xmlData)) = true THEN
-- get the node name
replaceTag := regexp_replace(xPathExpression, '/.*/', '');
-- replace the existing node with the newly passed in node
newXmlData := xml(regexp_replace(xmlData::text, '<'||replaceTag||'>.*</'||replaceTag||'>', xmlNode));
-- if node doesn't exist then we have to add it manually
ELSE
newXmlData := xml(REPLACE(xmlData::text, '</ImageXmlData>', xmlNode||'</ImageXmlData>'));
END IF;
UPDATE exam_files SET xml_data = newXmlData WHERE id = examFileId;
ELSE
-- there is no xml found so create xml from scratch and insert the node
xmlData := '<ImageXmlData>'||xmlNode||'</ImageXmlData>';
UPDATE exam_files SET xml_data = xmlData WHERE id = examFileId;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Glad you got a solution. To be honest, string formatting functions tend to be a bit difficult to reliably use inside SGML due to issues relating to hierarchies of languages. I.e. regexps have hard limits as to what they can do.
A better solution is likely to be to go a very different direction and write your functions in PL/PerlU or PL/Python, and use existing XML processing capabilities for those languages. This is likely to give you a better and more robust solution.

SQL Server 2008: Rename an element using XML DML?

Is it possible to use an XML DML statement to rename an element in an untyped XML column?
I am in the process of updating an XML Schema Collection on an XML column and need to patch the existing XML instances by renaming one element before I can apply the latest schema.
As far as I can tell from the docs you can only insert / delete nodes or replace their value.
As the saying goes, "Where there's a will there's a way"
Here's two methods:
the first is to simply replace the previous xml with a new xml constructed from the original with the new element name. In my example I've changed Legs/Leg to Limbs/Limb this could get very complicated for anything but the simplest schema
And secondly, a more appropriate approach of combining insert and delete.
I've combined them into one simple example:
declare #xml as xml = '<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>'
set #xml = (select
t.c.value('#species', 'varchar(max)') as '#species'
,(select
ti.C.value('.', 'varchar(max)')
from #Xml.nodes('//animal/legs/leg') ti(c) for xml path('limb'), /* root('limb'), */type) as limbs
from #xml.nodes('//*:animal') t(c) for xml path('animal'), type)
select #xml;
while (#xml.exist('/animal/limbs/limb') = 1) begin
/*insert..*/
set #xml.modify('
insert <leg>{/animal/limbs/limb[1]/text()}</leg>
before (/animal/limbs/limb)[1]
');
/*delete..*/
set #xml.modify('delete (/animal/limbs/limb)[1]');
end
set #xml.modify('
insert <legs>{/animal/limbs/leg}</legs>
before (/animal/limbs)[1]
');
set #xml.modify('delete (/animal/limbs)[1]');
select #xml;
During development of SQL Server Unit Test (ssut - see related blog post) I wanted to standardize an xml set coming from a tested object. As I will call the tested object multiple times, each time the set and record names will be the same. For reading ease, I want the record set from the original records to be named similar to <original_record_set><original_record /></original_record_set> and the record set for
test records to be named similar to <test_record_set><test_record /></ test_record_set >.
Obviously this is trivial to do if you can modify the call in the tested object as first:
SET #output = (SELECT col1, col2
FROM #test_object_result
FOR xml path ( test_record '), root( test_record_set '));
and then:
SET #output = (SELECT col1, col2
FROM #test_object_result
FOR xml path ( original_record'), root( original_record_set '));
However, since I'm calling the SAME object multiple times, and "for xml path" does NOT allow variables in the path('...') and root('...') methods, I had to come up with a different method.
This function accepts an xml tree and builds a new tree, replacing the root node with the value of #relation_name and the name of each record with #tuple_name. The new tree is built with all the attributes of the original, even if there are different numbers per record.
EXCEPTIONS
Obviously this does NOT work with multiple element levels! I have built it specifically to handle a single level attribute based tree as shown in the example below. I may build it out for a multi-level mixed attribute/element tree in the future, but I think that the method to do so becomes obvious now that I've solved the basic problem as below, and will leave that exercise to the reader pending that time.
USE [unit_test];
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[standardize_record_set]') AND type IN ( N'FN', N'IF', N'TF', N'FS', N'FT' ))
DROP FUNCTION [dbo].[standardize_record_set];
GO
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
SET nocount ON;
GO
/*
DECLARE
#relation_name nvarchar(150)= N'standardized_record_set',
#tuple_name nvarchar(150)= N'standardized_record',
#xml xml,
#standardized_result xml;
SET #xml='<Root>
<row id="12" two="now1" three="thr1" four="four1" />
<row id="232" two="now22" three="thr22" />
<row id="233" two="now23" three="thr23" threeextra="extraattrinthree" />
<row id="234" two="now24" three="thr24" fourextra="mealsoin four rwo big mone" />
<row id="235" two="now25" three="thr25" />
</Root>';
execute #standardized_result = [dbo].[standardize_record_set] #relation_name=#relation_name, #tuple_name=#tuple_name, #xml=#xml;
select #standardized_result;
*/
CREATE FUNCTION [dbo].[standardize_record_set] (#relation_name nvarchar(150)= N'record_set',
#tuple_name nvarchar(150)= N'record', #xml xml )
returns XML
AS
BEGIN
DECLARE
#attribute_index int = 1,
#attribute_count int = 0,
#record_set xml = N'<' + #relation_name + ' />',
#record_name nvarchar(50) = #tuple_name,
#builder nvarchar(max),
#record xml,
#next_record xml;
DECLARE #record_table TABLE (
record xml );
INSERT INTO #record_table
SELECT t.c.query('.') AS record
FROM #xml.nodes('/*/*') T(c);
DECLARE record_table_cursor CURSOR FOR
SELECT cast([record] AS xml)
FROM #record_table
OPEN record_table_cursor
FETCH NEXT FROM record_table_cursor INTO #next_record
WHILE ##FETCH_STATUS = 0
BEGIN
SET #attribute_index=1;
SET #attribute_count = #next_record.query('count(/*[1]/#*)').value('.', 'int');
SET #builder = N'<' + #record_name + N' ';
-- build up attribute string
WHILE #attribute_index <= #attribute_count
BEGIN
SET #builder = #builder + #next_record.value('local-name((/*/#*[sql:variable("#attribute_index")])[1])',
'varchar(max)') + '="' + #next_record.value('((/*/#*[sql:variable("#attribute_index")])[1])',
'varchar(max)') + '" ';
SET #attribute_index = #attribute_index + 1
END
-- build record and add to record_set
SET #record = #builder + ' />';
SET #record_set.modify('insert sql:variable("#record") into (/*)[1]');
FETCH NEXT FROM record_table_cursor INTO #next_record
END
CLOSE record_table_cursor;
DEALLOCATE record_table_cursor;
RETURN #record_set;
END;
GO
Yes you can use DML to rename an element by snipping it at the node you want renamed, injecting a new node at that element and then pasting the snipped elements back into the xml at that node. Ive done a SQL fiddle to demo. http://sqlfiddle.com/#!3/dc64d/1
This will change
<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>
into
<animal species="Mouse">
<armsandlegs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</armsandlegs>
</animal>
SqlFiddle looks to have long since broken my solution. From memory ive pasted the basis of my solution below...
DECLARE #XML2 xml
DECLARE #XML3 xml = '<limbs></limbs>'
DECLARE #XML xml =
'<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>'
SET #XML2 = #XML.query('animal/legs/*')
SET #XML.modify('
insert
(sql:variable("#XML3"))
after
(/animal/legs)[1]
')
SET #XML.modify('
delete (/animal/legs[1])
')
SET #XML.modify('
insert
(sql:variable("#XML2"))
as last into
(/animal/limbs)[1]
')
select #XML