Converting delimited values into a comma separated list - mysql

My Webapp (PHP/jQuery/MySQL) has features which enable me to send out nicely formatted html email notifications to my customers based on certain events. The code works nicely and merges data from my Database into form fields although I need to enhance it to be able to provide enriched/localised/reformatted data in some circumstances.
For example:
- Provide date/time values in a user's own timezone
- Provide monetary values formatted to a user's locale
This requires me to do another pass of the email content to detect whether any fields remain unmerged before sending the email off to the user and if so, to format those field values appropriately before sending the email. Therefore what I want to do is extract a list of all delimited fieldnames from a table field value and return that list in comma delimited form.
I can already count how many times a delimeter appears
I can also find the position of the first delimeter
It looks like it would be easy to split the values if I was using the same opening and closing delimeters but because I have many email templates already in use, this isn't currently viable
I don't have any code for this yet. I'm just trying to avoid writing my own MySQL function to do this, by using existing MySQL functions if they are capable of doing this.
I've tried using various combinations of SUBSTRING, SUBSTRING_INDEX, LOCATE.
So what I need to be able to do is something like this:
SELECT msg_id, values_found_between(msg_content,"<",">") AS comma_delimited_list;
So for example, with source data of...
msg_id | msg_content
-------+------------
1 | The quick brown <fox> jumps over the lazy <dog>
2 | The quick brown fox jumps over the lazy dog
I can get a resulting recordset such as this:
msg_id | comma_seperated_list
-------+------------
1 | fox,dog
Alright, I had a crack and this seems to work well:
CREATE FUNCTION db.`FN_find_values_between`(`in_haystack` VARCHAR(10000), `in_opening_delimiter` VARCHAR(1),`in_closing_delimiter` VARCHAR(1)) RETURNS varchar(1000) CHARSET utf8
BEGIN
DECLARE numFoundOpen INT DEFAULT 0;
DECLARE numFoundClose INT DEFAULT 0;
DECLARE numFoundTarget INT DEFAULT 0;
DECLARE numCurrentIndex INT DEFAULT 0;
DECLARE strOutput VARCHAR(1000) DEFAULT "";
DECLARE numSearchFromPos INT DEFAULT 1;
DECLARE numCurrentCharPosStart INT DEFAULT 1;
DECLARE numCurrentCharPosEnd INT DEFAULT 1;
DECLARE strCurrentFieldname VARCHAR(50) DEFAULT "";
DECLARE numLength INT DEFAULT 0;
SET numFoundOpen=
(SELECT
ROUND ((LENGTH(in_haystack)- LENGTH( REPLACE (in_haystack, in_opening_delimiter, ""))) / LENGTH(in_opening_delimiter)));
SET numFoundClose=
(SELECT
ROUND ((LENGTH(in_haystack)- LENGTH( REPLACE (in_haystack, in_closing_delimiter, ""))) / LENGTH(in_closing_delimiter)));
IF (numFoundOpen=numFoundClose) THEN
SET numFoundTarget=numFoundOpen;
END IF;
WHILE numCurrentIndex < numFoundTarget DO
SET numCurrentIndex=numCurrentIndex+1;
SET numCurrentCharPosStart = LOCATE(in_opening_delimiter, in_haystack, numSearchFromPos);
SET numCurrentCharPosEnd = LOCATE(in_closing_delimiter, in_haystack, numSearchFromPos);
SET numLength=1+(numCurrentCharPosEnd-numCurrentCharPosStart);
SET strCurrentFieldname=SUBSTRING(in_haystack,numCurrentCharPosStart,numLength);
SET strOutput=CONCAT(strOutput,strCurrentFieldname,",");
SET strCurrentFieldname="";
SET numSearchFromPos=numCurrentCharPosEnd+1;
END WHILE;
IF (strOutput <> "") THEN
SET strOutput=LEFT(strOutput,LENGTH(strOutput)-1);
END IF;
RETURN strOutput;
END;

As per the code above, I managed to write my own MySQL function to do this.

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.

unescape diactrics in \u0 format (json) in ms sql (SQL Server)

I'm getting json file, which I load to Azure SQL databese. This json is direct output from API, so there is nothing I can do with it before loading to DB.
In that file, all Polish diactircs are escaped to "C/C++/Java source code" (based on: http://www.fileformat.info/info/unicode/char/0142/index.htm
So for example:
ł is \u0142
I was trying to find some method to convert (unescape) those to proper Polish letters.
In worse case scenario, I can write function which will replace all combinations
Repalce(Replace(Replace(string,'\u0142',N'ł'),'\u0144',N'ń')))
And so on, making one big, terrible function...
I was looking for some ready functions like there is for URLdecode, which was answered here on stack in many topics, and here: https://www.codeproject.com/Articles/1005508/URL-Decode-in-T-SQL
Using this solution would be possible but I cannot figure out cast/convert with proper collation and types in there, to get result I'm looking for.
So if anyone knows/has function that would make conversion in string for unescaping that \u this would be great, but I will manage to write something on my own if I would get right conversion. For example I tried:
select convert(nvarchar(1), convert(varbinary, 0x0142, 1))
I made assumption that changing \u to 0x will be the answer but it gives some Chinese characters. So this is wrong direction...
Edit:
After googling more I found exactly same question here on stack from #Pasetchnik: Json escape unicode in SQL Server
And it looks this would be the best solution that there is in MS SQL.
Onlty thing I needed to change was using NVARCHAR instead of VARCHAR that is in linked solution:
CREATE FUNCTION dbo.Json_Unicode_Decode(#escapedString nVARCHAR(MAX))
RETURNS nVARCHAR(MAX)
AS
BEGIN
DECLARE #pos INT = 0,
#char nvarCHAR,
#escapeLen TINYINT = 2,
#hexDigits TINYINT = 4
SET #pos = CHARINDEX('\u', #escapedString, #pos)
WHILE #pos > 0
BEGIN
SET #char = NCHAR(CONVERT(varbinary(8), '0x' + SUBSTRING(#escapedString, #pos + #escapeLen, #hexDigits), 1))
SET #escapedString = STUFF(#escapedString, #pos, #escapeLen + #hexDigits, #char)
SET #pos = CHARINDEX('\u', #escapedString, #pos)
END
RETURN #escapedString
END
Instead of nested REPLACE you could use:
DECLARE #string NVARCHAR(MAX)= N'\u0142 \u0144\u0142';
SELECT #string = REPLACE(#string,u, ch)
FROM (VALUES ('\u0142',N'ł'),('\u0144', N'ń')) s(u, ch);
SELECT #string;
DBFiddle Demo

Mysql recursive procedure

I have a table name css_diectory with 3 columns directory_id, directory-name and root_directory. I am storing hierarchical directory structure in this table. I have written a procedure to retrieve all the descendants given the directory_id. But it doesn't work. Can anyone please help me.
Here is the procedure
CREATE PROCEDURE getDescendants
(IN rootId varchar(32), INOUT desecendantsFolderId varchar(3200))
BEGIN
DECLARE endOfRecord INTEGER DEFAULT 0;
DECLARE folderId varchar(32) DEFAULT "";
DECLARE folderName varchar(32) DEFAULT "";
DECLARE folderCursor CURSOR FOR
Select directory_id, directory_name from css_directory where root_directory=rootId;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET endOfRecord = 1;
OPEN folderCursor;
getFolderId: LOOP
FETCH folderCursor INTO folderId, folderName;
IF endOfRecord = 1 THEN
LEAVE getFolderId;
END IF;
Call getDescendants(folderId,desecendantsFolderId);
SET desecendantsFolderId = CONCAT(folderId,",",desecendantsFolderId);
call getDescendants(folderId,desecendantsFolderId);
END LOOP getFolderId;
END
Edit: The output of this procedure is always a null set. It does not produce any error
I'm not 100% sure I follow what you're doing, but it looks like there's a problem here:
SET desecendantsFolderId = CONCAT(folderId,",",desecendantsFolderId);
When any argument to CONCAT() is null, then the return value is null. Presumably descendantsFolderId is initially null, and if so, I don't see where that would change.
There are several ways to remedy this, but here is one of them:
SET desecendantsFolderId = NULLIF(CONCAT_WS(",",folderId,desecendantsFolderId),"");
CONCAT_WS() is like CONCAT(), except the first argument is used as a separator amd null arguments beyond the first one are disregarded, and empty string is returned if all subsequent arguments are null. NULLIF() is probably not technically needed, based on the rest of the code, but will ensure that the final result of CONCAT_WS() will be turned back to null if the input args are indeed all null.

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.

Create a MySQL procedure variable which is initialized when not provided

I need to create a MYSQL procedure where the procedure accepts several parameters and works with them. However, in the case where it is not present, the parameter variables pick some 'default' values and continue. Similar to how the pseudo-function-overload is handled in PHP.
This code is what I could come up with.
CREATE PROCEDURE PROC_INS_CONTENT_TEST(IN DATA_VAL LONGTEXT)
BEGIN
IF (DATA_VAL IS NULL) THEN SET DATA_VAL='DEFAULT'; END IF;
INSERT INTO CONTENT_TEST (DATA) VALUES (DATA_VAL);
END
And this code does not work the way I want it to behave. Is there a way to assign the default value to the variable right when the parameter is declared?
Yes there is.
If you want to set default value on variable on your function/procedure you can do this:
CREATE PROCEDURE PROC_INS_CONTENT_TEST(IN DATA_VAL LONGTEXT)
BEGIN
DECLARE my_data_double DOUBLE DEFAULT 0;
DECLARE my_data_varchar VARCHAR(20) DEFAULT 'default_value';
DECLARE DATA_VAL LONGTEXT DEFAULT 'default_value';
DECLARE my_data_integer INT DEFAULT 0;
END
Or if you want to assign value you can do something like:
SET my_data_value = 12345;
inside your function/procedure.