Browse Source

DECIMAL(P,S) as int/float/binary depending on P and S

Viktor Söderqvist 10 years ago
parent
commit
2d6d089841
3 changed files with 70 additions and 22 deletions
  1. 12 15
      doc/overview.edoc
  2. 22 4
      src/mysql_protocol.erl
  3. 36 3
      test/mysql_tests.erl

+ 12 - 15
doc/overview.edoc

@@ -51,11 +51,11 @@ connecting to and interacting with a MySQL server.
     <tr>
       <td>VARCHAR, TEXT, etc.</td>
       <td>`iodata()' [<a href="#vn1">1</a>]</td>
-      <td>`<<"foo">>, "bar"'</td>
+      <td>`<<"foo">>', `"bar"'</td>
     </tr>
     <tr>
       <td>BIT(N)</td>
-      <td>`bitstring()'</td>
+      <td>`<<_:N/bitstring>>'</td>
       <td>`<<255, 6:3>>'</td>
     </tr>
     <tr>
@@ -64,9 +64,11 @@ connecting to and interacting with a MySQL server.
       <td>`3.14'</td>
     </tr>
     <tr>
-      <td>DECIMAL</td>
-      <td>`binary()' [<a href="#vn2">2</a>]</td>
-      <td>`<<"3.140">>'</td>
+      <td>DECIMAL(P, S)</td>
+      <td>`integer()' when S == 0<br />
+          `float()' when P =&lt; 15 and S &gt; 0<br />
+          `binary()' when P &gt;= 16 and S &gt; 0 [<a href="#vn2">2</a>]</td>
+      <td>`42'<br />`3.14'<br />`<<"3.14159265358979323846">>'</td>
     </tr>
     <tr>
       <td>DATETIME, TIMESTAMP</td>
@@ -97,15 +99,10 @@ Notes:
 <ol>
   <li id="vn1">When fetching VARCHAR, TEXT etc. they are returned as `binary()'.
     When sending (insert or update) any `iodata()' is accepted.</li>
-  <li id="vn2">DECIMAL are currently always returned as `binary()'. This will
-    probably be changed to something similar to how the `odbc' OTP application
-    handles them, namely
-    <ul>
-      <li>`integer()' when there are no fractions;</li>
-      <li>`float()' when the precision of a float is exact enough;</li>
-      <li>`binary()' when the precision of a float is not enough.</li>
-    </ul>
-  </li>
+  <li id="vn2">DECIMALs are returned as `integer()' or `float()' when the value
+    can be represented without precision loss and as `binary()' for high
+    precision DECIMAL values. This is similar to how the `odbc' OTP application
+    treats DECIMALs.</li>
   <li id="vn3">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.)</li>
@@ -116,7 +113,7 @@ Notes:
 
 <h2>Copying</h2>
 
-Copyright 2014 The contributors of MySQL/OTP. See the project page at
+Copyright 2014 The authors of MySQL/OTP. See the project page at
 <a href="https://github.com/mysql-otp/mysql-otp"
    target="_top">https://github.com/mysql-otp/mysql-otp</a>.
 

+ 22 - 4
src/mysql_protocol.erl

@@ -401,13 +401,18 @@ decode_text(#col{type = T}, Text)
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
-       T == ?TYPE_GEOMETRY; T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
+       T == ?TYPE_GEOMETRY ->
     %% 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.
     Text;
 decode_text(#col{type = ?TYPE_BIT, length = Length}, Text) ->
     %% Convert to <<_:Length/bitstring>>
     decode_bitstring(Text, Length);
+decode_text(#col{type = T, decimals = S, length = L}, Text)
+  when T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
+    %% Length is the max number of symbols incl. dot and minus sign, e.g. the
+    %% number of digits plus 2.
+    decode_decimal(Text, L - 2, S);
 decode_text(#col{type = ?TYPE_DATE},
             <<Y:4/binary, "-", M:2/binary, "-", D:2/binary>>) ->
     {binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)};
@@ -543,7 +548,7 @@ decode_binary(#col{type = T}, Data)
   when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
        T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
        T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
-       T == ?TYPE_GEOMETRY; T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
+       T == ?TYPE_GEOMETRY ->
     %% 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.
     lenenc_str(Data);
@@ -558,6 +563,12 @@ decode_binary(#col{type = T}, <<Value:16/signed-little, Rest/binary>>)
     {Value, Rest};
 decode_binary(#col{type = ?TYPE_TINY}, <<Value:8/signed, Rest/binary>>) ->
     {Value, Rest};
+decode_binary(#col{type = T, decimals = S, length = L}, Data)
+  when T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
+    %% Length is the max number of symbols incl. dot and minus sign, e.g. the
+    %% number of digits plus 2.
+    {Binary, Rest} = lenenc_str(Data),
+    {decode_decimal(Binary, L - 2, S), Rest};
 decode_binary(#col{type = ?TYPE_DOUBLE},
               <<Value:64/float-little, Rest/binary>>) ->
     {Value, Rest};
@@ -765,6 +776,13 @@ encode_bitstring(Bitstring) ->
     PaddingSize = byte_size(Bitstring) * 8 - Size,
     <<0:PaddingSize, Bitstring:Size/bitstring>>.
 
+decode_decimal(Bin, _P, 0) ->
+    binary_to_integer(Bin);
+decode_decimal(Bin, P, S) when P =< 15, S > 0 ->
+    binary_to_float(Bin);
+decode_decimal(Bin, P, S) when P >= 16, S > 0 ->
+    Bin.
+
 %% @doc Converts a set of atoms (or binaries) to a comma-separated binary.
 set_to_binary(Set) ->
     List = [if is_atom(X) -> atom_to_binary(X, utf8); is_binary(X) -> X end
@@ -967,8 +985,8 @@ decode_text_test() ->
                   [?TYPE_FLOAT, ?TYPE_DOUBLE]),
     %% Decimal types
     lists:foreach(fun (T) ->
-                      ColDef = #col{type = T},
-                      ?assertEqual(<<"3.0">>, decode_text(ColDef, <<"3.0">>))
+                      ColDef = #col{type = T, decimals = 1, length = 4},
+                      ?assertMatch(3.0, decode_text(ColDef, <<"3.0">>))
                   end,
                   [?TYPE_DECIMAL, ?TYPE_NEWDECIMAL]),
     ?assertEqual(3.0,  decode_text(#col{type = ?TYPE_FLOAT}, <<"3">>)),

+ 36 - 3
test/mysql_tests.erl

@@ -61,6 +61,7 @@ query_test_() ->
              fun text_protocol/1,
              fun binary_protocol/1,
              fun float_rounding/1,
+             fun decimal/1,
              fun int/1,
              fun bit/1,
              fun time/1,
@@ -103,7 +104,7 @@ text_protocol(Pid) ->
     {ok, Columns, Rows} = mysql:query(Pid, <<"SELECT * FROM t">>),
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
-    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>, {0, {0, 22, 11}},
+    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, 3.14, {0, {0, 22, 11}},
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                  Rows),
 
@@ -118,7 +119,7 @@ binary_protocol(Pid) ->
     {ok, Ins} = mysql:prepare(Pid, <<"INSERT INTO t (bl, f, dc, ti, ts, da, c)"
                                      " VALUES (?, ?, ?, ?, ?, ?, ?)">>),
 
-    ok = mysql:execute(Pid, Ins, [<<"blob">>, 3.14, <<"3.14">>,
+    ok = mysql:execute(Pid, Ins, [<<"blob">>, 3.14, 3.14,
                                   {0, {0, 22, 11}}, 
                                   {{2014, 11, 03}, {0, 22, 24}},
                                   {2014, 11, 03}, null]),
@@ -130,7 +131,7 @@ binary_protocol(Pid) ->
     {ok, Columns, Rows} = mysql:execute(Pid, Stmt, [1]),
     ?assertEqual([<<"id">>, <<"bl">>, <<"tx">>, <<"f">>, <<"dc">>, <<"ti">>,
                   <<"ts">>, <<"da">>, <<"c">>], Columns),
-    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, <<"3.140">>,
+    ?assertEqual([[1, <<"blob">>, <<>>, 3.14, 3.14,
                    {0, {0, 22, 11}},
                    {{2014, 11, 03}, {00, 22, 24}}, {2014, 11, 03}, null]],
                  Rows),
@@ -190,6 +191,38 @@ float_rounding(Pid) ->
                 TestData),
     ok = mysql:query(Pid, "DROP TABLE f").
 
+decimal(Pid) ->
+    %% As integer when S == 0
+    ok = mysql:query(Pid, "CREATE TABLE dec0 (d DECIMAL(50, 0))"),
+    write_read_text_binary(
+        Pid, 14159265358979323846264338327950288419716939937510,
+        <<"14159265358979323846264338327950288419716939937510">>,
+        <<"dec0">>, <<"d">>
+    ),
+    write_read_text_binary(
+        Pid, -14159265358979323846264338327950288419716939937510,
+        <<"-14159265358979323846264338327950288419716939937510">>,
+        <<"dec0">>, <<"d">>
+    ),
+    ok = mysql:query(Pid, "DROP TABLE dec0"),
+    %% As float when P =< 15, S > 0
+    ok = mysql:query(Pid, "CREATE TABLE dec15 (d DECIMAL(15, 14))"),
+    write_read_text_binary(Pid, 3.14159265358979, <<"3.14159265358979">>,
+                           <<"dec15">>, <<"d">>),
+    write_read_text_binary(Pid, -3.14159265358979, <<"-3.14159265358979">>,
+                           <<"dec15">>, <<"d">>),
+    write_read_text_binary(Pid, 3.0, <<"3">>, <<"dec15">>, <<"d">>),
+    ok = mysql:query(Pid, "DROP TABLE dec15"),
+    %% As binary when P >= 16, S > 0
+    ok = mysql:query(Pid, "CREATE TABLE dec16 (d DECIMAL(16, 15))"),
+    write_read_text_binary(Pid, <<"3.141592653589793">>,
+                           <<"3.141592653589793">>, <<"dec16">>, <<"d">>),
+    write_read_text_binary(Pid, <<"-3.141592653589793">>,
+                           <<"-3.141592653589793">>, <<"dec16">>, <<"d">>),
+    write_read_text_binary(Pid, <<"3.000000000000000">>, <<"3">>,
+                           <<"dec16">>, <<"d">>),
+    ok = mysql:query(Pid, "DROP TABLE dec16").
+
 int(Pid) ->
     ok = mysql:query(Pid, "CREATE TABLE ints (i INT)"),
     write_read_text_binary(Pid, 42, <<"42">>, <<"ints">>, <<"i">>),