This is the first function I've ever done in MySQL, so I'm in trouble about how to make it do what I want. The function is syntactically correct, but it doesn't work.
The goal of this function should be to delete, from a JSON object, all the items containing a key named "checked", as the following example:
{
"type":{
"checked":"True", <---
"editable":"0",
"data_type":"string",
"value_name":"type",
"value_type":"-",
"value_unit":"-",
"raw_attribute":{
},
"healthiness_value":"300",
"healthiness_criteria":"refresh_rate"
},
"category":{
"checked":"True", <---
"editable":"0",
"data_type":"string",
"value_name":"category",
"value_type":"-",
"value_unit":"",
"raw_attribute":{
},
"healthiness_value":"300",
"healthiness_criteria":"refresh_rate"
},
"occupier":{
"checked":"False", <---
"editable":"0",
"data_type":"string",
"value_name":"occupier",
"value_type":"-",
"value_unit":"",
"raw_attribute":{
},
...
}
}
So, what I want is to have a function that filter, and show me when I do a query, only attributes that are checked "True" or "False", in consequence of the attribute checked given in the following SQL function:
This is saved inside a tuple of a MySQL db, under the column "attribute".
CREATE FUNCTION delete_checked(attributes JSON, checked VARCHAR(7))
RETURNS JSON
BEGIN
DECLARE attributes_keys JSON;
DECLARE iterator INT;
DECLARE attribute VARCHAR(30);
DECLARE _attributes JSON;
DECLARE _path VARCHAR(100);
SET attributes_keys = JSON_KEYS(attributes);
SET _attributes = attributes;
WHILE iterator > 0 DO
SET attribute = JSON_EXTRACT(attributes_keys, CONCAT("$[", iterator, "]"));
SET _path = CONCAT('$.', attribute, '.checked');
IF JSON_CONTAINS(_attributes, checked, _path) THEN
SET _attributes = JSON_REMOVE(attributes, CONCAT('$.', attribute));
END IF;
SET iterator = iterator - 1;
END WHILE; RETURN _attributes;
END//
Use case: "SELECT delete_checked(attributes, "False") FROM table WHERE ...". Otherwise, I should make this by filtering this out of db, but I don't like this way.
WHILE iterator > 0 DO
your loop won't start since iterator is only declared but not set.
CREATE FUNCTION my_function (json_doc JSON, checked VARCHAR(5))
RETURNS TEXT
RETURN ( SELECT JSON_OBJECTAGG(one_key, output)
FROM ( SELECT one_key, JSON_EXTRACT(json_doc, CONCAT('$.', one_key)) output
FROM JSON_TABLE(JSON_KEYS(json_doc),
'$[*]' COLUMNS (one_key VARCHAR(64) PATH '$')) jsontable
HAVING output->>'$.checked' = checked ) subquery );
USAGE: SELECT ..., my_function(table.json_column, 'True | False'), ...
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=71943335ae59d9836e6309e2a3a380ab
Related
a MYSQL user defined function selects a row from a table. How does the UDF code determine if the selected row was found in the table?
CREATE FUNCTION snippetFolder_folderPath(folder_id int)
RETURNS varchar(512)
BEGIN
declare vFolder_id int;
declare vParent_id int;
declare vPath varchar(512) default '';
declare vFolderName varchar(256) default '';
set vFolder_id = folder_id;
build_path:
while (vFolder_id > 0) do
/* -------- how to know this select statement returns a row?? ---------- */
select a.parent_id, a.folderName
into vParent_id, vFolderName
from SnippetFolder a
where a.folder_id = vFolder_id;
if vPath = ' ' then
set vPath = vFolderName;
else
set vPath = concat_ws( '/', vFolderName, vPath );
end if ;
set vFolder_id = vParent_id;
end while ;
return vPath;
END
https://dev.mysql.com/doc/refman/8.0/en/select-into.html says:
If the query returns no rows, a warning with error code 1329 occurs (No data), and the variable values remain unchanged.
So you could declare a continue handler on warnings, something like the example from the documentation:
BEGIN
DECLARE i INT DEFAULT 3;
DECLARE done INT DEFAULT FALSE;
retry:
REPEAT
BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING
BEGIN
SET done = TRUE;
END;
IF done OR i < 0 THEN
LEAVE retry;
END IF;
SET i = i - 1;
END;
UNTIL FALSE END REPEAT;
END
I'll leave it to you to read the documentation and adapt that example to your table and your loop.
Alternatively, if you're using MySQL 8.0 you can use recursive common table expression:
CREATE FUNCTION snippetFolder_folderPath(vFolder_id int)
RETURNS varchar(512)
BEGIN
DECLARE vPath varchar(512) DEFAULT '';
WITH RECURSIVE cte AS (
SELECT folderName, parent_id, 0 AS height
FROM SnippetFolder WHERE folder_id = vFolder_id
UNION
SELECT f.folderName, f.parent_id, cte.height+1
FROM SnippetFolder AS f JOIN cte ON cte.parent_id = f.folder_id
)
SELECT GROUP_CONCAT(folderName ORDER BY height DESC SEPARATOR '/')
INTO vPath
FROM cte;
RETURN vPath;
END
The recursive CTE result is all the ancestors of the row matching vFolder_id, and then one can use GROUP_CONCAT() to concatenate them together as one string.
The question is about MySQL/MariaDB JSON Functions.
How do you find intersection of multiple JSON structures?
In PHP it is done using this code:
array_intersect(
['a', 'b'],
['b', 'c']
);
If we imagine a function named JSON_INTERSECT, the code would look like this:
SET #json1 = '{"key1": "a", "key2": "b"}';
SET #json2 = '["b", "c"]';
SET #json3 = '["c", "d"]';
SELECT JSON_INTERSECT(#json1, #json2); // returns '["b"]';
SELECT JSON_INTERSECT(#json1, #json3); // returns NULL;
It looks there are no good built-in ways of doing this and there are still no good answers on this topic, so I thought I'd add my quick & dirty solution. If you execute the below code it will create a function called MY_JSON_INTERSECT that will output results exactly as the original poster specified. Make sure you've looked this over and are ok with creating a new function before trusting my code:
delimiter $$
CREATE FUNCTION `MY_JSON_INTERSECT`(Array1 VARCHAR(1024), Array2 VARCHAR(1024)) RETURNS varchar(1024)
BEGIN
DECLARE x int;
DECLARE val, output varchar(1024);
SET output = '[]';
SET x = 0;
IF JSON_LENGTH(Array2) < JSON_LENGTH(Array1) THEN
SET val = Array2;
SET Array2 = Array1;
SET Array1 = val;
END IF;
WHILE x < JSON_LENGTH(Array1) DO
SET val = JSON_EXTRACT(Array1, CONCAT('$[',x,']'));
IF JSON_CONTAINS(Array2,val) THEN
SET output = JSON_MERGE(output,val);
END IF;
SET x = x + 1;
END WHILE;
IF JSON_LENGTH(output) = 0 THEN
RETURN NULL;
ELSE
RETURN output;
END IF;
END$$
You can then call the function like this:
SELECT MY_JSON_INTERSECT('[1,2,3,4,5,6,7,8]','[0,3,5,7,9]');
Outputs:
[3,5,7]
This isn't beautiful or efficient, but it's something that works... Hopefully better answers will come soon.
I need to receive a json string as a parameter of the procedure and parse it as a json object.
An example of this json string: {'new_settings': [{'setting_name': 'test', 'setting_value': 'test_value'}]}
I need to fetch the array property named "new_settings" and iterate through the objects inside this array.
This is what i have at the moment:
CREATE PROCEDURE `sp_test` (IN `_settings` longtext)
BEGIN
DECLARE i INT UNSIGNED DEFAULT 0;
DECLARE count INT UNSIGNED DEFAULT 0;
DECLARE current_item LONGTEXT;
START TRANSACTION;
SET count = (JSON_LENGTH(JSON_EXTRACT(_settings, '$.new_settings')) - 1);
WHILE i < count DO
SET current_item := (_settings, CONCAT('$[', i, ']'));
INSERT INTO settings(setting_name, setting_value) VALUES (JSON_EXTRACT(current_item, '$.setting_name'), JSON_EXTRACT(current_item, '$.setting_value'));
SET i := i + 1;
END WHILE;
COMMIT;
END
But i am getting the following error: "Syntax error in JSON text in argument 1 to function 'json_extract' at position 3" and i dont understand what is wrong.
Can you provide some insight?
Thank you in advance.
Your json is all wrong.
You always should check with json Validators, if mysql can read your input.
SET #a = '{"new_settings": [{"setting_name": "test", "setting_value": "test_value"}]}';
SELECT JSON_EXTRACT(#a, '$.new_settings[0].setting_name') c1
, JSON_EXTRACT(#a, '$.new_settings[0].setting_value') c2;
Would give you
c1 c2
test test_value
Where did you get your json String?
I try to do this
CREATE FUNCTION getOneCentOrderIds (s text) RETURNS text
BEGIN
DECLARE no_more_orders, ent_id INT default 0;
DECLARE ids text;
DECLARE orders_cur CURSOR FOR SELECT entity_id FROM sales_flat_order WHERE total_due = 0.01;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_orders = 1;
OPEN orders_cur;
FETCH NEXT FROM orders_cur INTO ent_id;
REPEAT
SET ids = CONCAT(ids, ', ', ent_id);
FETCH orders_cur INTO ent_id;
UNTIL no_more_orders END REPEAT;
CLOSE orders_cur;
RETURN ids;
END$
but I get null when I execute the function.
If I simply remove concat and leave SET ids = ent_id I get the last id in cursor, as expected.
How should I do the concatenation ?
The concat() function returns NULL if any of its arguments are NULL. Try
DECLARE ids text DEFAULT '';
which will make sure the first call to CONCAT has no NULL arguments.
Instead of creating a function, the above can simple be done in a query as
SELECT group_concat(entity_id) FROM sales_flat_order WHERE total_due = 0.01;
I want to be able to pass an "array" of values to my stored procedure, instead of calling "Add value" procedure serially.
Can anyone suggest a way to do it? am I missing something here?
Edit: I will be using PostgreSQL / MySQL, I haven't decided yet.
As Chris pointed, in PostgreSQL it's no problem - any base type (like int, text) has it's own array subtype, and you can also create custom types including composite ones. For example:
CREATE TYPE test as (
n int4,
m int4
);
Now you can easily create array of test:
select ARRAY[
row(1,2)::test,
row(3,4)::test,
row(5,6)::test
];
You can write a function that will multiply n*m for each item in array, and return sum of products:
CREATE OR REPLACE FUNCTION test_test(IN work_array test[]) RETURNS INT4 as $$
DECLARE
i INT4;
result INT4 := 0;
BEGIN
FOR i IN SELECT generate_subscripts( work_array, 1 ) LOOP
result := result + work_array[i].n * work_array[i].m;
END LOOP;
RETURN result;
END;
$$ language plpgsql;
and run it:
# SELECT test_test(
ARRAY[
row(1, 2)::test,
row(3,4)::test,
row(5,6)::test
]
);
test_test
-----------
44
(1 row)
If you plan to use MySQL 5.1, it is not possible to pass in an array.
See the MySQL 5.1 faq
If you plan to use PostgreSQL, it is possible look here
I don't know about passing an actual array into those engines (I work with sqlserver) but here's an idea for passing a delimited string and parsing it in your sproc with this function.
CREATE FUNCTION [dbo].[Split]
(
#ItemList NVARCHAR(4000),
#delimiter CHAR(1)
)
RETURNS #IDTable TABLE (Item VARCHAR(50))
AS
BEGIN
DECLARE #tempItemList NVARCHAR(4000)
SET #tempItemList = #ItemList
DECLARE #i INT
DECLARE #Item NVARCHAR(4000)
SET #tempItemList = REPLACE (#tempItemList, ' ', '')
SET #i = CHARINDEX(#delimiter, #tempItemList)
WHILE (LEN(#tempItemList) > 0)
BEGIN
IF #i = 0
SET #Item = #tempItemList
ELSE
SET #Item = LEFT(#tempItemList, #i - 1)
INSERT INTO #IDTable(Item) VALUES(#Item)
IF #i = 0
SET #tempItemList = ''
ELSE
SET #tempItemList = RIGHT(#tempItemList, LEN(#tempItemList) - #i)
SET #i = CHARINDEX(#delimiter, #tempItemList)
END
RETURN
END
You didn't indicate, but if you are referring to SQL server, here's one way.
And the MS support ref.
For PostgreSQL, you could do something like this:
CREATE OR REPLACE FUNCTION fnExplode(in_array anyarray) RETURNS SETOF ANYELEMENT AS
$$
SELECT ($1)[s] FROM generate_series(1,array_upper($1, 1)) AS s;
$$
LANGUAGE SQL IMMUTABLE;
Then, you could pass a delimited string to your stored procedure.
Say, param1 was an input param containing '1|2|3|4|5'
The statement:
SELECT CAST(fnExplode(string_to_array(param1, '|')) AS INTEGER);
results in a result set that can be joined or inserted.
Likewise, for MySQL, you could do something like this:
DELIMITER $$
CREATE PROCEDURE `spTest_Array`
(
v_id_arr TEXT
)
BEGIN
DECLARE v_cur_position INT;
DECLARE v_remainder TEXT;
DECLARE v_cur_string VARCHAR(255);
CREATE TEMPORARY TABLE tmp_test
(
id INT
) ENGINE=MEMORY;
SET v_remainder = v_id_arr;
SET v_cur_position = 1;
WHILE CHAR_LENGTH(v_remainder) > 0 AND v_cur_position > 0 DO
SET v_cur_position = INSTR(v_remainder, '|');
IF v_cur_position = 0 THEN
SET v_cur_string = v_remainder;
ELSE
SET v_cur_string = LEFT(v_remainder, v_cur_position - 1);
END IF;
IF TRIM(v_cur_string) != '' THEN
INSERT INTO tmp_test
(id)
VALUES
(v_cur_string);
END IF;
SET v_remainder = SUBSTRING(v_remainder, v_cur_position + 1);
END WHILE;
SELECT
id
FROM
tmp_test;
DROP TEMPORARY TABLE tmp_test;
END
$$
Then simply CALL spTest_Array('1|2|3|4|5') should produce the same result set as the above PostgreSQL query.
Thanks to JSON support in MySQL you now actually have the ability to pass an array to your MySQL stored procedure. Create a JSON_ARRAY and simply pass it as a JSON argument to your stored procedure.
Then in procedure, using MySQL's WHILE loop and MySQL's JSON "pathing" , access each of the elements in the JSON_ARRAY and do as you wish.
An example here https://gist.githubusercontent.com/jonathanvx/513066eea8cb5919b648b2453db47890/raw/22f33fdf64a2f292688edbc67392ba2ccf8da47c/json.sql
Incidently, here is how you would add the array to a function (stored-proc) call:
CallableStatement proc = null;
List<Integer> faultcd_array = Arrays.asList(1003, 1234, 5678);
//conn - your connection manager
conn = DriverManager.getConnection(connection string here);
proc = conn.prepareCall("{ ? = call procedureName(?) }");
proc.registerOutParameter(1, Types.OTHER);
//This sets-up the array
Integer[] dataFaults = faultcd_array.toArray(new Integer[faultcd_array.size()]);
java.sql.Array sqlFaultsArray = conn.createArrayOf("int4", dataFaults);
proc.setArray(2, sqlFaultsArray);
//:
//add code to retrieve cursor, use the data.
//: