Browse Source

Add support for the MySQL JSON type

This commit adds support for the JSON data type introduced
in MySQL 5.7.8.

The unit test for JSON is very fragile as it compares strings
directly. It should use a proper JSON parser and compare the
parsed terms.
Johan Lövdahl 8 years ago
parent
commit
5c473d2ecf
3 changed files with 52 additions and 10 deletions
  1. 1 0
      include/protocol.hrl
  2. 2 2
      src/mysql_protocol.erl
  3. 49 8
      test/mysql_tests.erl

+ 1 - 0
include/protocol.hrl

@@ -113,6 +113,7 @@
 -define(TYPE_YEAR, 16#0d).
 -define(TYPE_YEAR, 16#0d).
 -define(TYPE_VARCHAR, 16#0f).
 -define(TYPE_VARCHAR, 16#0f).
 -define(TYPE_BIT, 16#10).
 -define(TYPE_BIT, 16#10).
+-define(TYPE_JSON, 16#f5).
 -define(TYPE_NEWDECIMAL, 16#f6).
 -define(TYPE_NEWDECIMAL, 16#f6).
 -define(TYPE_ENUM, 16#f7).
 -define(TYPE_ENUM, 16#f7).
 -define(TYPE_SET, 16#f8).
 -define(TYPE_SET, 16#f8).

+ 2 - 2
src/mysql_protocol.erl

@@ -459,7 +459,7 @@ decode_text(#col{type = T}, Text)
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
-       T == ?TYPE_GEOMETRY ->
+       T == ?TYPE_GEOMETRY; T == ?TYPE_JSON ->
     %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
     %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
     %% cannot convert them to atom() or sets:set(), etc.
     %% cannot convert them to atom() or sets:set(), etc.
     Text;
     Text;
@@ -607,7 +607,7 @@ decode_binary(#col{type = T}, Data)
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
-       T == ?TYPE_GEOMETRY ->
+       T == ?TYPE_GEOMETRY; T == ?TYPE_JSON ->
     %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
     %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
     %% cannot convert them to atom() or sets:set(), etc.
     %% cannot convert them to atom() or sets:set(), etc.
     lenenc_str(Data);
     lenenc_str(Data);

+ 49 - 8
test/mysql_tests.erl

@@ -176,6 +176,7 @@ query_test_() ->
           {"DATE",                 fun () -> date(Pid) end},
           {"DATE",                 fun () -> date(Pid) end},
           {"TIME",                 fun () -> time(Pid) end},
           {"TIME",                 fun () -> time(Pid) end},
           {"DATETIME",             fun () -> datetime(Pid) end},
           {"DATETIME",             fun () -> datetime(Pid) end},
+          {"JSON",                 fun () -> json(Pid) end},
           {"Microseconds",         fun () -> microseconds(Pid) end}]
           {"Microseconds",         fun () -> microseconds(Pid) end}]
      end}.
      end}.
 
 
@@ -308,7 +309,7 @@ binary_protocol(Pid) ->
                                      " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)">>),
                                      " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)">>),
     %% 16#161 is the codepoint for "s with caron"; <<197, 161>> in UTF-8.
     %% 16#161 is the codepoint for "s with caron"; <<197, 161>> in UTF-8.
     ok = mysql:execute(Pid, Ins, [<<"blob">>, [16#161], 3.14, 3.14, 3.14,
     ok = mysql:execute(Pid, Ins, [<<"blob">>, [16#161], 3.14, 3.14, 3.14,
-                                  2014, {0, {0, 22, 11}}, 
+                                  2014, {0, {0, 22, 11}},
                                   {{2014, 11, 03}, {0, 22, 24}},
                                   {{2014, 11, 03}, {0, 22, 24}},
                                   {2014, 11, 03}, null]),
                                   {2014, 11, 03}, null]),
 
 
@@ -486,15 +487,43 @@ datetime(Pid) ->
     ),
     ),
     ok = mysql:query(Pid, "DROP TABLE dt").
     ok = mysql:query(Pid, "DROP TABLE dt").
 
 
+json(Pid) ->
+    Version = db_version_string(Pid),
+    try
+        Version1 = parse_db_version(Version),
+        Version1 >= [5, 7, 8] orelse throw(nope)
+    of _ ->
+        test_valid_json(Pid),
+        test_invalid_json(Pid)
+    catch _:_ ->
+        error_logger:info_msg("Skipping JSON test. Current MySQL"
+                              " version is ~s. Required version is >= 5.7.8.~n",
+                              [Version])
+    end.
+
+test_valid_json(Pid) ->
+    ok = mysql:query(Pid, "CREATE TABLE json_t (json_c JSON)"),
+    Value = <<"'{\"a\": 1, \"b\": {\"c\": [1, 2, 3, 4]}}'">>,
+    Expected = <<"{\"a\": 1, \"b\": {\"c\": [1, 2, 3, 4]}}">>,
+    write_read_text_binary(Pid, Expected, Value,
+                           <<"json_t">>, <<"json_c">>),
+    ok = mysql:query(Pid, "DROP TABLE json_t").
+
+test_invalid_json(Pid) ->
+    ok = mysql:query(Pid, "CREATE TABLE json_t (json_c JSON)"),
+    InvalidJson = <<"'{\"a\": \"c\": 2}'">>,
+    ?assertMatch({error,{3140, <<"22032">>, _}},
+                 mysql:query(Pid, <<"INSERT INTO json_t (json_c)"
+                                    " VALUES (", InvalidJson/binary,
+                                    ")">>)),
+    ok = mysql:query(Pid, "DROP TABLE json_t").
+
 microseconds(Pid) ->
 microseconds(Pid) ->
     %% Check whether we have the required version for this testcase.
     %% Check whether we have the required version for this testcase.
-    {ok, _, [[Version]]} = mysql:query(Pid, <<"SELECT @@version">>),
+    Version = db_version_string(Pid),
     try
     try
-        %% Remove stuff after dash for e.g. "5.5.40-0ubuntu0.12.04.1-log"
-        [Version1 | _] = binary:split(Version, <<"-">>),
-        Version2 = lists:map(fun binary_to_integer/1,
-                             binary:split(Version1, <<".">>, [global])),
-        Version2 >= [5, 6, 4] orelse throw(nope)
+        Version1 = parse_db_version(Version),
+        Version1 >= [5, 6, 4] orelse throw(nope)
     of _ ->
     of _ ->
         test_time_microseconds(Pid),
         test_time_microseconds(Pid),
         test_datetime_microseconds(Pid)
         test_datetime_microseconds(Pid)
@@ -531,7 +560,8 @@ write_read_text_binary(Conn, Term, SqlLiteral, Table, Column) ->
     InsertQuery = <<"INSERT INTO ", Table/binary, " (", Column/binary, ")"
     InsertQuery = <<"INSERT INTO ", Table/binary, " (", Column/binary, ")"
                     " VALUES (", SqlLiteral/binary, ")">>,
                     " VALUES (", SqlLiteral/binary, ")">>,
     ok = mysql:query(Conn, InsertQuery),
     ok = mysql:query(Conn, InsertQuery),
-    ?assertEqual({ok, [Column], [[Term]]}, mysql:query(Conn, SelectQuery)),
+    R = mysql:query(Conn, SelectQuery),
+    ?assertEqual({ok, [Column], [[Term]]}, R),
     ?assertEqual({ok, [Column], [[Term]]}, mysql:execute(Conn, SelectStmt, [])),
     ?assertEqual({ok, [Column], [[Term]]}, mysql:execute(Conn, SelectStmt, [])),
     mysql:query(Conn, <<"DELETE FROM ", Table/binary>>),
     mysql:query(Conn, <<"DELETE FROM ", Table/binary>>),
 
 
@@ -645,3 +675,14 @@ gen_server_coverage_test() ->
     {noreply, state} = mysql:handle_cast(foo, state),
     {noreply, state} = mysql:handle_cast(foo, state),
     {noreply, state} = mysql:handle_info(foo, state),
     {noreply, state} = mysql:handle_info(foo, state),
     ok = mysql:terminate(kill, state).
     ok = mysql:terminate(kill, state).
+
+%% --- Utility functions
+db_version_string(Pid) ->
+  {ok, _, [[Version]]} = mysql:query(Pid, <<"SELECT @@version">>),
+  Version.
+
+parse_db_version(Version) ->
+  %% Remove stuff after dash for e.g. "5.5.40-0ubuntu0.12.04.1-log"
+  [Version1 | _] = binary:split(Version, <<"-">>),
+  lists:map(fun binary_to_integer/1,
+            binary:split(Version1, <<".">>, [global])).