Flite Careers

Replacing JSON UDF Calls With Native Function Calls in MySQL 5.7

If you use JSON UDFs in MySQL 5.6 there are a few things to consider before upgrading to MySQL 5.7. From a schema perspective, if you have JSON stored in text columns you can decide when to convert those columns to the new JSON datatype after you upgrade, but due to namespace collisions and format differences it’s probably not safe to upgrade without dropping some or all of the existing UDFs and updating your queries that use those UDFs.

Read on for details…

Namespace collisions

There are 15 functions in JSON UDFs version 0.4.0. Nine of those names are used by native JSON functions in MySQL 5.7, so it’s not possible to continue using those nine UDFs in MySQL 5.7. Therefore before you upgrade you should probably drop these UDFs:

1
2
3
4
5
6
7
8
9
DROP FUNCTION IF EXISTS json_append;
DROP FUNCTION IF EXISTS json_depth;
DROP FUNCTION IF EXISTS json_extract;
DROP FUNCTION IF EXISTS json_merge;
DROP FUNCTION IF EXISTS json_remove;
DROP FUNCTION IF EXISTS json_replace;
DROP FUNCTION IF EXISTS json_search;
DROP FUNCTION IF EXISTS json_set;
DROP FUNCTION IF EXISTS json_valid;

Path syntax

Depending on which version of MySQL JSON UDFs you are using, you may need to rewrite your queries to use the new path syntax. If you are using version 0.2 or 0.3 then you’re using this path syntax:

1
json_extract(doc, keypart1, keypart2, ...)

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select json_extract('{"foo":"bar"}','foo');
+-------------------------------------+
| json_extract('{"foo":"bar"}','foo') |
+-------------------------------------+
| bar                                 |
+-------------------------------------+
1 row in set (0.00 sec)

mysql> select json_extract('{"parent":{"child":"hello"}}','parent','child');
+---------------------------------------------------------------+
| json_extract('{"parent":{"child":"hello"}}','parent','child') |
+---------------------------------------------------------------+
| hello                                                         |
+---------------------------------------------------------------+
1 row in set (0.00 sec)

The native JSON functions use this path syntax:

1
JSON_EXTRACT(json_doc, path[, path] ...)

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select json_extract('{"foo":"bar"}','$.foo');
+---------------------------------------+
| json_extract('{"foo":"bar"}','$.foo') |
+---------------------------------------+
| "bar"                                 |
+---------------------------------------+
1 row in set (0.00 sec)

mysql> select json_extract('{"parent":{"child":"hello"}}','$.parent.child');
+---------------------------------------------------------------+
| json_extract('{"parent":{"child":"hello"}}','$.parent.child') |
+---------------------------------------------------------------+
| "hello"                                                       |
+---------------------------------------------------------------+
1 row in set (0.00 sec)

MySQL JSON UDFs version 0.4 had two copies of each UDF – one for each path syntax – so you only need to rewrite this part of your queries if you’re using the old path syntax. The functions using the old path syntax are in libmy_json_udf and the functions using the new path syntax are in libmy_json_udf_path.

Input differences

The MySQL 5.7 native JSON functions have some input differences as compared to the JSON UDF functions with the same name. One fundamental difference is the support for input strings which are not valid JSON. In general, the UDFs return NULL when the input is not valid JSON, whereas the native functions return an error. If your input is a column of JSON datatype then this is not a concern, since the datatype enforces the validity of the JSON and will not store invalid JSON. If the input is text you need to be more careful.

For example, none of these queries throw an error in MySQL 5.6 using JSON UDFs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select json_extract('','foo');
+------------------------+
| json_extract('','foo') |
+------------------------+
| NULL                   |
+------------------------+
1 row in set (0.00 sec)

mysql> select json_extract('NOT JSON','foo');
+--------------------------------+
| json_extract('NOT JSON','foo') |
+--------------------------------+
| NULL                           |
+--------------------------------+
1 row in set (0.00 sec)

You can simply use COALESCE() if you want to replace the NULL output with the empty string:

1
2
3
4
5
6
7
mysql> select coalesce(json_extract('NOT JSON','foo'),'');
+---------------------------------------------+
| coalesce(json_extract('NOT JSON','foo'),'') |
+---------------------------------------------+
|                                             |
+---------------------------------------------+
1 row in set (0.00 sec)

But in MySQL 5.7 with native functions these queries throw an error:

1
2
3
4
mysql> select json_extract('','$.foo');
ERROR 3141 (22032): Invalid JSON text in argument 1 to function json_extract: "The document is empty." at position 0.
mysql> select json_extract('NOT JSON','$.foo');
ERROR 3141 (22032): Invalid JSON text in argument 1 to function json_extract: "Invalid value." at position 0.

One way around that is using a CASE statement along with the JSON_VALID() functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> set @json = '';
Query OK, 0 rows affected (0.00 sec)

mysql> select (case when json_valid(@json) then json_extract(@json,'$.foo') else '' end);
+----------------------------------------------------------------------------+
| (case when json_valid(@json) then json_extract(@json,'$.foo') else '' end) |
+----------------------------------------------------------------------------+
|                                                                            |
+----------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> set @json = 'NOT JSON';
Query OK, 0 rows affected (0.01 sec)

mysql> select (case when json_valid(@json) then json_extract(@json,'$.foo') else '' end);
+----------------------------------------------------------------------------+
| (case when json_valid(@json) then json_extract(@json,'$.foo') else '' end) |
+----------------------------------------------------------------------------+
|                                                                            |
+----------------------------------------------------------------------------+
1 row in set (0.00 sec)

Since the above queries return empty string if they encounter invalid JSON there is no need to use COALESCE().

Output differences

The MySQL 5.7 native JSON functions return JSON objects, whereas the JSON UDFs return text. For example a call to the JSON_EXTRACT() UDF in MySQL 5.6 may return foo, but the same function in MySQL 5.7 would return "foo". Therefore in order to replace a call to the UDF version of a function like JSON_EXTRACT() with the corresponding native function call, you should wrap that call with the JSON_UNQUOTE() function.

Examples:

MySQL 5.6

1
2
3
4
5
6
7
mysql> select json_extract('{"foo":"bar"}','foo');
+-------------------------------------+
| json_extract('{"foo":"bar"}','foo') |
+-------------------------------------+
| bar                                 |
+-------------------------------------+
1 row in set (0.00 sec)

MySQL 5.7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select json_extract('{"foo":"bar"}','$.foo');
+---------------------------------------+
| json_extract('{"foo":"bar"}','$.foo') |
+---------------------------------------+
| "bar"                                 |
+---------------------------------------+
1 row in set (0.12 sec)

mysql> select json_unquote(json_extract('{"foo":"bar"}','$.foo'));
+-----------------------------------------------------+
| json_unquote(json_extract('{"foo":"bar"}','$.foo')) |
+-----------------------------------------------------+
| bar                                                 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

While you are re-writing those function calls, consider using the -> (added in 5.7.9) and ->> (added in 5.7.13) operators, which are shortcuts for JSON_EXTRACT() and JSON_UNQUOTE(JSON_EXTRACT()) respectively. For example, these three statements are equivalent:

1
2
3
4
5
6
7
8
select JSON_UNQUOTE( JSON_EXTRACT(my_column, '$.some_key') )
from my_table;

select JSON_UNQUOTE(my_column -> '$.some_key')
from my_table;

select my_column->>'$.some_key'
from my_table;

Testing those out to confirm the output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mysql> create temporary table if not exists json_test as select '{"foo":"bar"}' as json_val;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> select json_unquote(json_extract(json_val,'$.foo')) from json_test;
+----------------------------------------------+
| json_unquote(json_extract(json_val,'$.foo')) |
+----------------------------------------------+
| bar                                          |
+----------------------------------------------+
1 row in set (0.03 sec)

mysql> select json_unquote(json_val->'$.foo') from json_test;
+---------------------------------+
| json_unquote(json_val->'$.foo') |
+---------------------------------+
| bar                             |
+---------------------------------+
1 row in set (0.00 sec)

mysql> select json_val->>'$.foo' from json_test;
+--------------------+
| json_val->>'$.foo' |
+--------------------+
| bar                |
+--------------------+
1 row in set (0.00 sec)
Comments

Comments