Browse Source

Negative TIME, microseconds in TIME and DATETIME, new TIME representation

Viktor Söderqvist 10 years ago
parent
commit
1e0ff94b5b
3 changed files with 178 additions and 39 deletions
  1. 8 1
      README.md
  2. 68 34
      src/mysql_protocol.erl
  3. 102 4
      test/mysql_tests.erl

+ 8 - 1
README.md

@@ -83,9 +83,16 @@ FLOAT, DOUBLE       | float()                 | 3.14
 DECIMAL             | binary()                | <<"3.140">>
 DECIMAL             | binary()                | <<"3.140">>
 DATETIME, TIMESTAMP | calendar:datetime()     | {{2014, 11, 18}, {10, 22, 36}}
 DATETIME, TIMESTAMP | calendar:datetime()     | {{2014, 11, 18}, {10, 22, 36}}
 DATE                | calendar:date()         | {2014, 11, 18}
 DATE                | calendar:date()         | {2014, 11, 18}
-TIME                | {time, calendar:time()} | {time, {10, 22, 36}} -- **will probably change**
+TIME                | {Days, calendar:time()} | {0, {10, 22, 36}}
 NULL                | null                    | null
 NULL                | null                    | null
 
 
+Since `TIME` can be outside the calendar:time() interval, we use the format as
+returned by `calendar:seconds_to_daystime/1` for `TIME` values.
+
+For `DATETIME`, `TIMESTAMP` and `TIME` values with franctions of seconds, we use
+a float for the seconds part. (These are unusual and were added to MySQL in
+version 5.6.4.)
+
 Tests
 Tests
 -----
 -----
 
 

+ 68 - 34
src/mysql_protocol.erl

@@ -384,17 +384,39 @@ decode_text(T, Text)
     Text;
     Text;
 decode_text(?TYPE_DATE, <<Y:4/binary, "-", M:2/binary, "-", D:2/binary>>) ->
 decode_text(?TYPE_DATE, <<Y:4/binary, "-", M:2/binary, "-", D:2/binary>>) ->
     {binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)};
     {binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)};
-decode_text(?TYPE_TIME, <<H:2/binary, ":", Mi:2/binary, ":", S:2/binary>>) ->
-    %% FIXME: Hours can be negative + more digits. Seconds can have fractions.
-    %% Add tests for these cases.
-    Time = {binary_to_integer(H), binary_to_integer(Mi), binary_to_integer(S)},
-    {time, Time};
+decode_text(?TYPE_TIME, Text) ->
+    {match, [Sign, Hbin, Mbin, Sbin, Frac]} =
+        re:run(Text,
+               <<"^(-?)(\\d+):(\\d+):(\\d+)(\\.?\\d*)$">>,
+               [{capture, all_but_first, binary}]),
+    H = binary_to_integer(Hbin),
+    M = binary_to_integer(Mbin),
+    S = binary_to_integer(Sbin),
+    IsNeg = Sign == <<"-">>,
+    Fraction = case Frac of
+        <<>> -> 0;
+        _ when not IsNeg -> binary_to_float(<<"0", Frac/binary>>);
+        _ when IsNeg -> 1 - binary_to_float(<<"0", Frac/binary>>)
+    end,
+    Sec1 = H * 3600 + M * 60 + S,
+    Sec2 = if IsNeg -> -Sec1; true -> Sec1 end,
+    Sec3 = if IsNeg and (Fraction /= 0) -> Sec2 - 1;
+              true                      -> Sec2
+           end,
+    {Days, {Hours, Minutes, Seconds}} = calendar:seconds_to_daystime(Sec3),
+    {Days, {Hours, Minutes, Seconds + Fraction}};
 decode_text(T, <<Y:4/binary, "-", M:2/binary, "-", D:2/binary, " ",
 decode_text(T, <<Y:4/binary, "-", M:2/binary, "-", D:2/binary, " ",
                  H:2/binary, ":", Mi:2/binary, ":", S:2/binary>>)
                  H:2/binary, ":", Mi:2/binary, ":", S:2/binary>>)
   when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME ->
   when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME ->
-    %% FIXME: Fractions of seconds.
+    %% Without fractions.
     {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)},
     {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)},
      {binary_to_integer(H), binary_to_integer(Mi), binary_to_integer(S)}};
      {binary_to_integer(H), binary_to_integer(Mi), binary_to_integer(S)}};
+decode_text(T, <<Y:4/binary, "-", M:2/binary, "-", D:2/binary, " ",
+                 H:2/binary, ":", Mi:2/binary, ":", FloatS/binary>>)
+  when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME ->
+    %% With fractions.
+    {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)},
+     {binary_to_integer(H), binary_to_integer(Mi), binary_to_float(FloatS)}};
 decode_text(T, Text) when T == ?TYPE_FLOAT; T == ?TYPE_DOUBLE ->
 decode_text(T, Text) when T == ?TYPE_FLOAT; T == ?TYPE_DOUBLE ->
     try binary_to_float(Text)
     try binary_to_float(Text)
     catch error:badarg ->
     catch error:badarg ->
@@ -574,18 +596,26 @@ decode_binary(?TYPE_TIME, <<Length, Data/binary>>) ->
     %% micro_seconds (4) -- micro-seconds
     %% micro_seconds (4) -- micro-seconds
     case {Length, Data} of
     case {Length, Data} of
         {0, _} ->
         {0, _} ->
-            {{time, {0, 0, 0}}, Data};
+            {{0, {0, 0, 0}}, Data};
         {8, <<0, D:32/little, H, M, S, Rest/binary>>} ->
         {8, <<0, D:32/little, H, M, S, Rest/binary>>} ->
-            {{time, {D * 24 + H, M, S}}, Rest};
+            {{D, {H, M, S}}, Rest};
         {12, <<0, D:32/little, H, M, S, Micro:32/little, Rest/binary>>} ->
         {12, <<0, D:32/little, H, M, S, Micro:32/little, Rest/binary>>} ->
-            {{time, {D * 24 + H, M, S + 0.000001 * Micro}}, Rest};
+            {{D, {H, M, S + 0.000001 * Micro}}, Rest};
         {8, <<1, D:32/little, H, M, S, Rest/binary>>} ->
         {8, <<1, D:32/little, H, M, S, Rest/binary>>} ->
-            %% Negative time. Negating H, M and S is correct but a bit strange.
-            %% We could recalulate like calendar:seconds_to_daystime/1 does:
-            %% {-1,{23,58,20}} = calendar:seconds_to_daystime(-100).
-            {{time, {-(D * 24 + H), -M, -S}}, Rest};
-        {12, <<1, D:32/little, H, M, S, Micro:32/little, Rest/binary>>} ->
-            {{time, {-(D * 24 + H), -M, -S - 0.000001 * Micro}}, Rest}
+            %% Negative time. Example: '-00:00:01' --> {-1,{23,59,59}}
+            Seconds = ((D * 24 + H) * 60 + M) * 60 + S,
+            %Seconds = D * 86400 + calendar:time_to_seconds({H, M, S}),
+            {calendar:seconds_to_daystime(-Seconds), Rest};
+        {12, <<1, D:32/little, H, M, S, Micro:32/little, Rest/binary>>}
+          when Micro > 0 ->
+            %% Negate and convert to seconds, excl fractions
+            Seconds = -(((D * 24 + H) * 60 + M) * 60 + S),
+            %Seconds = -D * 86400 - calendar:time_to_seconds({H, M, S}),
+            %% Subtract 1 second for the fractions
+            {Days, {Hours, Minutes, Sec}} =
+                calendar:seconds_to_daystime(Seconds - 1),
+            %% Adding the fractions to Sec again makes it a float
+            {{Days, {Hours, Minutes, Sec + 1 - 0.000001 * Micro}}, Rest}
     end.
     end.
 
 
 %% @doc Like trunc/1 but towards negative infinity instead of towards zero.
 %% @doc Like trunc/1 but towards negative infinity instead of towards zero.
@@ -653,26 +683,30 @@ encode_param({{Y, M, D}, {H, Mi, S}}) when is_float(S) ->
     %% similar to a datetime. Microseconds in MySQL timestamps are possible but
     %% similar to a datetime. Microseconds in MySQL timestamps are possible but
     %% not very common.
     %% not very common.
     Sec = trunc(S),
     Sec = trunc(S),
-    Micro = 1000000 * (S - Sec),
+    Micro = round(1000000 * (S - Sec)),
     {<<?TYPE_DATETIME, 0>>, <<11, Y:16/little, M, D, H, Mi, Sec,
     {<<?TYPE_DATETIME, 0>>, <<11, Y:16/little, M, D, H, Mi, Sec,
                               Micro:32/little>>};
                               Micro:32/little>>};
-encode_param({time, {H, M, S}}) ->
-    %% calendar:time() tagged with 'time'
-    {<<?TYPE_TIME, 0>>, binary_encode_seconds(H * 3600 + M * 60 + S)}.
-
-%% Helper to encode TIME values.
-binary_encode_seconds(Sec) when is_integer(Sec) ->
-    {NegFlag, AbsSec} = if Sec >= 0 -> {0, Sec};
-                           Sec <  0 -> {1, -Sec} end,
-    {Days, {H, M, S}} = calendar:seconds_to_daystime(AbsSec),
-    <<8, NegFlag, Days:32/little, H, M, S>>;
-binary_encode_seconds(Sec) when is_float(Sec) ->
-    {NegFlag, AbsSec} = if Sec >= 0 -> {0, Sec};
-                           Sec <  0 -> {1, -Sec} end,
-    SecInt = trunc(AbsSec),
-    Micro = trunc(1000000 * (AbsSec - SecInt)),
-    {Days, {H, M, S}} = calendar:seconds_to_daystime(SecInt),
-    <<12, NegFlag, Days:32/little, H, M, S, Micro:32/little>>.
+encode_param({D, {H, M, S}}) when is_integer(S), D >= 0 ->
+    %% calendar:seconds_to_daystime()
+    {<<?TYPE_TIME, 0>>, <<8, 0, D:32/little, H, M, S>>};
+encode_param({D, {H, M, S}}) when is_integer(S), D < 0 ->
+    %% Convert to seconds, negate and convert back to daystime form.
+    %% Then set the minus flag.
+    Seconds = ((D * 24 + H) * 60 + M) * 60 + S,
+    {D1, {H1, M1, S1}} = calendar:seconds_to_daystime(-Seconds),
+    {<<?TYPE_TIME, 0>>, <<8, 1, D1:32/little, H1, M1, S1>>};
+encode_param({D, {H, M, S}}) when is_float(S), D >= 0 ->
+    S1 = trunc(S),
+    Micro = round(1000000 * (S - S1)),
+    {<<?TYPE_TIME, 0>>, <<12, 0, D:32/little, H, M, S1, Micro:32/little>>};
+encode_param({D, {H, M, S}}) when is_float(S), S > 0.0, D < 0 ->
+    IntS = trunc(S),
+    Micro = round(1000000 * (1 - S + IntS)),
+    Seconds = (D * 24 + H) * 3600 + M * 60 + IntS + 1,
+    {D1, {M1, H1, S1}} = calendar:seconds_to_daystime(-Seconds),
+    {<<?TYPE_TIME, 0>>, <<12, 1, D1:32/little, H1, M1, S1, Micro:32/little>>};
+encode_param({D, {H, M, 0.0}}) ->
+    encode_param({D, {H, M, 0}}).
 
 
 %% -- Protocol basics: packets --
 %% -- Protocol basics: packets --
 
 
@@ -866,7 +900,7 @@ decode_text_test() ->
 
 
     %% Date and time
     %% Date and time
     ?assertEqual({2014, 11, 01}, decode_text(?TYPE_DATE, <<"2014-11-01">>)),
     ?assertEqual({2014, 11, 01}, decode_text(?TYPE_DATE, <<"2014-11-01">>)),
-    ?assertEqual({time, {23, 59, 01}}, decode_text(?TYPE_TIME, <<"23:59:01">>)),
+    ?assertEqual({0, {23, 59, 01}}, decode_text(?TYPE_TIME, <<"23:59:01">>)),
     ?assertEqual({{2014, 11, 01}, {23, 59, 01}},
     ?assertEqual({{2014, 11, 01}, {23, 59, 01}},
                  decode_text(?TYPE_DATETIME, <<"2014-11-01 23:59:01">>)),
                  decode_text(?TYPE_DATETIME, <<"2014-11-01 23:59:01">>)),
     ?assertEqual({{2014, 11, 01}, {23, 59, 01}},
     ?assertEqual({{2014, 11, 01}, {23, 59, 01}},

+ 102 - 4
test/mysql_tests.erl

@@ -54,7 +54,9 @@ query_test_() ->
      {with, [fun basic_queries/1,
      {with, [fun basic_queries/1,
              fun text_protocol/1,
              fun text_protocol/1,
              fun binary_protocol/1,
              fun binary_protocol/1,
-             fun float_rounding/1]}}.
+             fun float_rounding/1,
+             fun time/1,
+             fun microseconds/1]}}.
 
 
 basic_queries(Pid) ->
 basic_queries(Pid) ->
 
 
@@ -86,7 +88,7 @@ text_protocol(Pid) ->
     {ok, Columns, Rows} = mysql:query(Pid, <<"SELECT * FROM t">>),
     {ok, Columns, Rows} = mysql:query(Pid, <<"SELECT * FROM t">>),
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
-    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>, {time, {0, 22, 11}},
+    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>, {0, {0, 22, 11}},
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                  Rows),
                  Rows),
 
 
@@ -106,7 +108,7 @@ binary_protocol(Pid) ->
                                      " VALUES (?, ?, ?, ?, ?, ?, ?)">>),
                                      " VALUES (?, ?, ?, ?, ?, ?, ?)">>),
 
 
     ok = mysql:execute(Pid, Ins, [<<"blob">>, 3.14, <<"3.14">>,
     ok = mysql:execute(Pid, Ins, [<<"blob">>, 3.14, <<"3.14">>,
-                                  {time, {0, 22, 11}}, 
+                                  {0, {0, 22, 11}}, 
                                   {{2014, 11, 03}, {0, 22, 24}},
                                   {{2014, 11, 03}, {0, 22, 24}},
                                   {2014, 11, 03}, null]),
                                   {2014, 11, 03}, null]),
 
 
@@ -118,7 +120,7 @@ binary_protocol(Pid) ->
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
     ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>,
     ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>,
-                   {time, {0, 22, 11}},
+                   {0, {0, 22, 11}},
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                  Rows),
                  Rows),
 
 
@@ -179,3 +181,99 @@ float_rounding(Pid) ->
                 end,
                 end,
                 TestData),
                 TestData),
     ok = mysql:query(Pid, "DROP TABLE f").
     ok = mysql:query(Pid, "DROP TABLE f").
+
+%% Test TIME value representation. There are a few things to check.
+time(Pid) ->
+    ok = mysql:query(Pid, "CREATE TABLE tm (tm TIME)"),
+    {ok, Insert} = mysql:prepare(Pid, "INSERT INTO tm VALUES (?)"),
+    {ok, Select} = mysql:prepare(Pid, "SELECT tm FROM tm"),
+    lists:foreach(
+        fun ({Value, Text}) ->
+            %% --- Insert using text query ---
+            ok = mysql:query(Pid, ["INSERT INTO tm VALUES ('", Text, "')"]),
+            %% Select using prepared statement
+            ?assertEqual({ok, [<<"tm">>], [[Value]]},
+                         mysql:execute(Pid, Select, [])),
+            %% Select using plain query
+            ?assertEqual({ok, [<<"tm">>], [[Value]]},
+                         mysql:query(Pid, "SELECT tm FROM tm")),
+            %% Empty table
+            ok = mysql:query(Pid, "DELETE FROM tm"),
+            %% --- Insert using prepared statement ---
+            ok = mysql:execute(Pid, Insert, [Value]),
+            %% Select using prepared statement
+            ?assertEqual({ok, [<<"tm">>], [[Value]]},
+                         mysql:execute(Pid, Select, [])),
+            %% Select using plain query
+            ?assertEqual({ok, [<<"tm">>], [[Value]]},
+                         mysql:query(Pid, "SELECT tm FROM tm")),
+            %% Empty table
+            ok = mysql:query(Pid, "DELETE FROM tm"),
+            ok
+        end,
+        [{{0, {10, 11, 12}},   "10:11:12"},
+         {{5, {0, 0, 1}},     "120:00:01"},
+         {{-1, {23, 59, 59}}, "-00:00:01"},
+         {{-1, {23, 59, 0}},  "-00:01:00"},
+         {{-1, {23, 0, 0}},   "-01:00:00"},
+         {{-1, {0, 0, 0}},    "-24:00:00"},
+         {{-5, {10, 0, 0}},  "-110:00:00"}]
+    ),
+    ok = mysql:query(Pid, "DROP TABLE tm").
+
+microseconds(Pid) ->
+    %% Check whether we have the required version for this testcase.
+    {ok, _, [[VersionBin]]} = mysql:query(Pid, <<"SELECT @@version">>),
+    Version = lists:map(fun binary_to_integer/1,
+                        binary:split(VersionBin, <<".">>, [global])),
+    if
+        Version >= [5, 6, 4] ->
+            ok = mysql:query(Pid, "CREATE TABLE m (t TIME(6))"),
+            SelectTime = "SELECT t FROM m",
+            {ok, SelectStmt} = mysql:prepare(Pid, SelectTime),
+            {ok, InsertStmt} = mysql:prepare(Pid, "INSERT INTO m VALUES (?)"),
+            %% Positive time, insert using plain query
+            E1 = {0, {23, 59, 57.654321}},
+            ok = mysql:query(Pid,
+                             <<"INSERT INTO m VALUES ('23:59:57.654321')">>),
+            ?assertEqual({ok, [<<"t">>], [[E1]]},
+                         mysql:query(Pid, SelectTime)),
+            ?assertEqual({ok, [<<"t">>], [[E1]]},
+                         mysql:execute(Pid, SelectStmt, [])),
+            ok = mysql:query(Pid, "DELETE FROM m"),
+            %% The same, but insert using prepared stmt
+            ok = mysql:execute(Pid, InsertStmt, [E1]),
+            ?assertEqual({ok, [<<"t">>], [[E1]]},
+                         mysql:query(Pid, SelectTime)),
+            ?assertEqual({ok, [<<"t">>], [[E1]]},
+                         mysql:execute(Pid, SelectStmt, [])),
+            ok = mysql:query(Pid, "DELETE FROM m"),
+            %% Negative time
+            E2 = {-1, {23, 59, 57.654321}},
+            ok = mysql:query(Pid,
+                             <<"INSERT INTO m VALUES ('-00:00:02.345679')">>),
+            ?assertEqual({ok, [<<"t">>], [[E2]]},
+                         mysql:query(Pid, SelectTime)),
+            ?assertEqual({ok, [<<"t">>], [[E2]]},
+                         mysql:execute(Pid, SelectStmt, [])),
+            ok = mysql:query(Pid, "DELETE FROM m"),
+            %% The same, but insert using prepared stmt
+            ok = mysql:execute(Pid, InsertStmt, [E2]),
+            ?assertEqual({ok, [<<"t">>], [[E2]]},
+                         mysql:query(Pid, SelectTime)),
+            ?assertEqual({ok, [<<"t">>], [[E2]]},
+                         mysql:execute(Pid, SelectStmt, [])),
+            ok = mysql:query(Pid, "DROP TABLE m"),
+            %% Datetime
+            Q3 = <<"SELECT TIMESTAMP '2014-11-23 23:59:57.654321' AS t">>,
+            E3 = [[{{2014, 11, 23}, {23, 59, 57.654321}}]],
+            ?assertEqual({ok, [<<"t">>], E3}, mysql:query(Pid, Q3)),
+            {ok, S3} = mysql:prepare(Pid, Q3),
+            ?assertEqual({ok, [<<"t">>], E3}, mysql:execute(Pid, S3, [])),
+            ok;
+        true ->
+            error_logger:info_msg("Skipping microseconds test. Microseconds are"
+                                  " not available on MySQL version ~s. Required"
+                                  " version is >= 5.6.4.",
+                                  [VersionBin])
+    end.