From d9c546d6c8ad303172208a31d85b8a64d0b383de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 1 Oct 2017 22:17:19 +0200 Subject: [PATCH 1/2] Support Elixir v1.3 calendar types --- lib/mariaex.ex | 2 + lib/mariaex/messages.ex | 18 +-- lib/mariaex/protocol.ex | 19 ++- lib/mariaex/query.ex | 15 ++ lib/mariaex/row_parser.ex | 308 +++++++++++++++++++++++--------------- mix.exs | 4 +- test/query_test.exs | 99 ++++++++++-- test/text_query_test.exs | 21 ++- 8 files changed, 335 insertions(+), 151 deletions(-) diff --git a/lib/mariaex.ex b/lib/mariaex.ex index 1357106..9869f5a 100644 --- a/lib/mariaex.ex +++ b/lib/mariaex.ex @@ -64,6 +64,8 @@ defmodule Mariaex do if not `DBConnection.Connection` (default: `DBConnection.Connection`); * `:name` - A name to register the started process (see the `:name` option in `GenServer.start_link/3`). + * `:datetime` - How datetimes should be returned. `:structs` for Elixir v1.3 + calendar types or `:tuples` for the backwards compatible tuples ## Function signatures diff --git a/lib/mariaex/messages.ex b/lib/mariaex/messages.ex index b65c7bc..85d1905 100644 --- a/lib/mariaex/messages.ex +++ b/lib/mariaex/messages.ex @@ -229,32 +229,32 @@ defmodule Mariaex.Messages do do: {nil, rest} def decode_bin_rows(<< len :: size(24)-little-integer, seqnum :: size(8)-integer, body :: size(len)-binary, rest :: binary>>, - fields, nullbin_size, rows) do + fields, nullbin_size, rows, datetime) do case body do <<0 :: 8, nullbin::size(nullbin_size)-little-unit(8), values :: binary>> -> - row = Mariaex.RowParser.decode_bin_rows(values, fields, nullbin) - decode_bin_rows(rest, fields, nullbin_size, [row | rows]) + row = Mariaex.RowParser.decode_bin_rows(values, fields, nullbin, datetime) + decode_bin_rows(rest, fields, nullbin_size, [row | rows], datetime) body -> msg = decode_msg(body, :bin_rows) {:ok, packet(size: len, seqnum: seqnum, msg: msg, body: body), rows, rest} end end - def decode_bin_rows(<>, _fields, _nullbin_size, rows) do + def decode_bin_rows(<>, _fields, _nullbin_size, rows, _datetime) do {:more, rows, rest} end def decode_text_rows(<< len :: size(24)-little-integer, seqnum :: size(8)-integer, body :: size(len)-binary, rest :: binary>>, - fields, rows) do + fields, rows, datetime) do case body do << 254 :: 8, _ :: binary >> = body when byte_size(body) < 9 -> msg = decode_msg(body, :text_rows) {:ok, packet(size: len, seqnum: seqnum, msg: msg, body: body), rows, rest} body -> - row = Mariaex.RowParser.decode_text_rows(body, fields) - decode_text_rows(rest, fields, [row | rows]) + row = Mariaex.RowParser.decode_text_rows(body, fields, datetime) + decode_text_rows(rest, fields, [row | rows], datetime) end end - def decode_text_rows(<>, _fields, rows) do + def decode_text_rows(<>, _fields, rows, _datetime) do {:more, rows, rest} end @@ -280,6 +280,6 @@ defmodule Mariaex.Messages do << contents :: size(length_nul_terminated)-binary, 0 :: 8 >> -> contents contents -> contents end - {String.strip(auth_plugin_data2, 0), next} + {String.trim(auth_plugin_data2, "\0"), next} end end diff --git a/lib/mariaex/protocol.ex b/lib/mariaex/protocol.ex index 2a17535..f301034 100644 --- a/lib/mariaex/protocol.ex +++ b/lib/mariaex/protocol.ex @@ -61,6 +61,7 @@ defmodule Mariaex.Protocol do cache: nil, cursors: %{}, seqnum: 0, + datetime: :structs, ssl_conn_state: :undefined # :undefined | :not_used | :ssl_handshake | :connected @doc """ @@ -74,6 +75,7 @@ defmodule Mariaex.Protocol do host = opts[:hostname] |> parse_host connect_opts = [host, opts[:port], opts[:socket_options], opts[:timeout]] binary_as = opts[:binary_as] || :field_type_var_string + datetime = opts[:datetime] || :structs case apply(sock_mod, :connect, connect_opts) do {:ok, sock} -> @@ -85,6 +87,7 @@ defmodule Mariaex.Protocol do cache: reset_cache(), lru_cache: reset_lru_cache(opts[:cache_size]), timeout: opts[:timeout], + datetime: datetime, opts: opts} handshake_recv(s, %{opts: opts}) {:error, reason} -> @@ -126,7 +129,7 @@ defmodule Mariaex.Protocol do end defp parse_host(host) do - host = if is_binary(host), do: String.to_char_list(host), else: host + host = if is_binary(host), do: String.to_charlist(host), else: host case :inet.parse_strict_address(host) do {:ok, address} -> @@ -323,8 +326,8 @@ defmodule Mariaex.Protocol do other end end - def handle_prepare(%Query{type: :binary} = query, _, s) do - case prepare_lookup(%Query{query | binary_as: s.binary_as}, s) do + def handle_prepare(%Query{type: :binary} = query, _, %{binary_as: binary_as} = s) do + case prepare_lookup(%Query{query | binary_as: binary_as}, s) do {:prepared, query} -> {:ok, query, s} {:prepare, query} -> @@ -528,8 +531,8 @@ defmodule Mariaex.Protocol do end end - defp text_row_decode(s, fields, rows, buffer) do - case decode_text_rows(buffer, fields, rows) do + defp text_row_decode(%{datetime: datetime} = s, fields, rows, buffer) do + case decode_text_rows(buffer, fields, rows, datetime) do {:ok, packet, rows, rest} -> {:ok, packet, rows, %{s | buffer: rest}} {:more, rows, rest} -> @@ -635,8 +638,8 @@ defmodule Mariaex.Protocol do end end - defp binary_row_decode(s, fields, nullbin_size, rows, buffer) do - case decode_bin_rows(buffer, fields, nullbin_size, rows) do + defp binary_row_decode(%{datetime: datetime} = s, fields, nullbin_size, rows, buffer) do + case decode_bin_rows(buffer, fields, nullbin_size, rows, datetime) do {:ok, packet, rows, rest} -> {:ok, packet, rows, %{s | buffer: rest}} {:more, rows, rest} -> @@ -1022,7 +1025,7 @@ defmodule Mariaex.Protocol do l |> Enum.map(&bxor(&1, extra - 64)) |> to_string end - defp hash(bin) when is_binary(bin), do: bin |> to_char_list |> hash + defp hash(bin) when is_binary(bin), do: bin |> to_charlist |> hash defp hash(s), do: hash(s, 1345345333, 305419889, 7) defp hash([c | s], n1, n2, add) do n1 = bxor(n1, (((band(n1, 63) + add) * c + n1 * 256))) diff --git a/lib/mariaex/query.ex b/lib/mariaex/query.ex index f7ab6da..b398bb6 100644 --- a/lib/mariaex/query.ex +++ b/lib/mariaex/query.ex @@ -111,6 +111,20 @@ defimpl DBConnection.Query, for: Mariaex.Query do bin = Decimal.to_string(value, :normal) {0, :field_type_newdecimal, << to_length_encoded_integer(byte_size(bin)) :: binary, bin :: binary >>} end + + defp encode_param(%Date{year: year, month: month, day: day}, _binary_as), + do: {0, :field_type_date, << 4::8-little, year::16-little, month::8-little, day::8-little>>} + defp encode_param(%Time{hour: hour, minute: min, second: sec, microsecond: {0, 0}}, _binary_as), + do: {0, :field_type_time, << 8 :: 8-little, 0 :: 8-little, 0 :: 32-little, hour :: 8-little, min :: 8-little, sec :: 8-little >>} + defp encode_param(%Time{hour: hour, minute: min, second: sec, microsecond: {msec, _}}, _binary_as), + do: {0, :field_type_time, << 12 :: 8-little, 0 :: 8-little, 0 :: 32-little, hour :: 8-little, min :: 8-little, sec :: 8-little, msec :: 32-little>>} + defp encode_param(%NaiveDateTime{year: year, month: month, day: day, + hour: hour, minute: min, second: sec, microsecond: {0, 0}}, _binary_as), + do: {0, :field_type_datetime, << 7::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little>>} + defp encode_param(%NaiveDateTime{year: year, month: month, day: day, + hour: hour, minute: min, second: sec, microsecond: {msec, _}}, _binary_as), + do: {0, :field_type_datetime, <<11::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little>>} + defp encode_param({year, month, day}, _binary_as), do: {0, :field_type_date, << 4::8-little, year::16-little, month::8-little, day::8-little>>} defp encode_param({hour, min, sec, 0}, _binary_as), @@ -123,6 +137,7 @@ defimpl DBConnection.Query, for: Mariaex.Query do do: {0, :field_type_datetime, << 7::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little>>} defp encode_param({{year, month, day}, {hour, min, sec, msec}}, _binary_as), do: {0, :field_type_datetime, <<11::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little>>} + defp encode_param(other, _binary_as), do: raise ArgumentError, "query has invalid parameter #{inspect other}" diff --git a/lib/mariaex/row_parser.ex b/lib/mariaex/row_parser.ex index 0084d86..fe3c23c 100644 --- a/lib/mariaex/row_parser.ex +++ b/lib/mariaex/row_parser.ex @@ -20,8 +20,8 @@ defmodule Mariaex.RowParser do {fields, div(length(fields) + 7 + 2, 8)} end - def decode_bin_rows(row, fields, nullint) do - decode_bin_rows(row, fields, nullint >>> 2, []) + def decode_bin_rows(row, fields, nullint, datetime) do + decode_bin_rows(row, fields, nullint >>> 2, [], datetime) end ## Helpers @@ -78,197 +78,252 @@ defmodule Mariaex.RowParser do defp type_to_atom({:bit, :field_type_bit}, _), do: :bit defp type_to_atom({:null, :field_type_null}, _), do: nil - defp decode_bin_rows(<>, [_ | fields], nullint, acc) when (nullint &&& 1) === 1 do - decode_bin_rows(rest, fields, nullint >>> 1, [nil | acc]) + defp decode_bin_rows(<>, [_ | fields], nullint, acc, datetime) when (nullint &&& 1) === 1 do + decode_bin_rows(rest, fields, nullint >>> 1, [nil | acc], datetime) end - defp decode_bin_rows(<>, [:string | fields], null_bitfield, acc) do - decode_string(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:string | fields], null_bitfield, acc, datetime) do + decode_string(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:uint8 | fields], null_bitfield, acc) do - decode_uint8(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:uint8 | fields], null_bitfield, acc, datetime) do + decode_uint8(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:int8 | fields], null_bitfield, acc) do - decode_int8(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:int8 | fields], null_bitfield, acc, datetime) do + decode_int8(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:uint16 | fields], null_bitfield, acc) do - decode_uint16(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:uint16 | fields], null_bitfield, acc, datetime) do + decode_uint16(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:int16 | fields], null_bitfield, acc) do - decode_int16(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:int16 | fields], null_bitfield, acc, datetime) do + decode_int16(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:uint32 | fields], null_bitfield, acc) do - decode_uint32(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:uint32 | fields], null_bitfield, acc, datetime) do + decode_uint32(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:int32 | fields], null_bitfield, acc) do - decode_int32(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:int32 | fields], null_bitfield, acc, datetime) do + decode_int32(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:uint64 | fields], null_bitfield, acc) do - decode_uint64(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:uint64 | fields], null_bitfield, acc, datetime) do + decode_uint64(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:int64 | fields], null_bitfield, acc) do - decode_int64(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:int64 | fields], null_bitfield, acc, datetime) do + decode_int64(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:time | fields], null_bitfield, acc) do - decode_time(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:time | fields], null_bitfield, acc, datetime) do + decode_time(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:date | fields], null_bitfield, acc) do - decode_date(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:date | fields], null_bitfield, acc, datetime) do + decode_date(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:datetime | fields], null_bitfield, acc) do - decode_datetime(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:datetime | fields], null_bitfield, acc, datetime) do + decode_datetime(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:decimal | fields], null_bitfield, acc) do - decode_decimal(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:decimal | fields], null_bitfield, acc, datetime) do + decode_decimal(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:float32 | fields], null_bitfield, acc) do - decode_float32(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:float32 | fields], null_bitfield, acc, datetime) do + decode_float32(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:float64 | fields], null_bitfield, acc) do - decode_float64(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:float64 | fields], null_bitfield, acc, datetime) do + decode_float64(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:bit | fields], null_bitfield, acc) do - decode_string(rest, fields, null_bitfield >>> 1, acc) + defp decode_bin_rows(<>, [:bit | fields], null_bitfield, acc, datetime) do + decode_string(rest, fields, null_bitfield >>> 1, acc, datetime) end - defp decode_bin_rows(<>, [:nil | fields], null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield >>> 1, [nil | acc]) + defp decode_bin_rows(<>, [:nil | fields], null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield >>> 1, [nil | acc], datetime) end - defp decode_bin_rows(<<>>, [], _, acc) do + defp decode_bin_rows(<<>>, [], _, acc, _datetime) do Enum.reverse(acc) end - defp decode_string(<>, fields, nullint, acc) when len <= 250 do - decode_bin_rows(rest, fields, nullint, [string | acc]) + defp decode_string(<>, fields, nullint, acc, datetime) when len <= 250 do + decode_bin_rows(rest, fields, nullint, [string | acc], datetime) end - defp decode_string(<<252::8, len::16-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc) do - decode_bin_rows(rest, fields, nullint, [string | acc]) + defp decode_string(<<252::8, len::16-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc, datetime) do + decode_bin_rows(rest, fields, nullint, [string | acc], datetime) end - defp decode_string(<<253::8, len::24-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc) do - decode_bin_rows(rest, fields, nullint, [string | acc]) + defp decode_string(<<253::8, len::24-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc, datetime) do + decode_bin_rows(rest, fields, nullint, [string | acc], datetime) end - defp decode_string(<<254::8, len::64-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc) do - decode_bin_rows(rest, fields, nullint, [string | acc]) + defp decode_string(<<254::8, len::64-little, string::size(len)-binary, rest::bits>>, fields, nullint, acc, datetime) do + decode_bin_rows(rest, fields, nullint, [string | acc], datetime) end - defp decode_float32(<>, fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [value | acc]) + defp decode_float32(<>, fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield, [value | acc], datetime) end - defp decode_float64(<>, fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [value | acc]) + defp decode_float64(<>, fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield, [value | acc], datetime) end defp decode_uint8(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_int8(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_uint16(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_int16(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_uint32(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_int32(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_uint64(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_int64(<>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield , [value | acc]) + fields, null_bitfield, acc, datetime) do + decode_bin_rows(rest, fields, null_bitfield , [value | acc], datetime) end defp decode_decimal(<>, - fields, null_bitfield, acc) do + fields, null_bitfield, acc, datetime) do value = Decimal.new(raw_value) - decode_bin_rows(rest, fields, null_bitfield, [value | acc]) + decode_bin_rows(rest, fields, null_bitfield, [value | acc], datetime) end defp decode_time(<< 0::8-little, rest::bits>>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{0, 0, 0, 0} | acc]) + fields, null_bitfield, acc, :structs) do + time = %Time{hour: 0, minute: 0, second: 0} + decode_bin_rows(rest, fields, null_bitfield, [time | acc], :structs) end defp decode_time(<<8::8-little, _::8-little, _::32-little, hour::8-little, min::8-little, sec::8-little, rest::bits>>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{hour, min, sec, 0} | acc]) + fields, null_bitfield, acc, :structs) do + time = %Time{hour: hour, minute: min, second: sec} + decode_bin_rows(rest, fields, null_bitfield, [time | acc], :structs) end defp decode_time(<< 12::8, _::32-little, _::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little, rest::bits >>, - fields, null_bitfield, acc) do + fields, null_bitfield, acc, :structs) do + time = %Time{hour: hour, minute: min, second: sec, microsecond: {msec, 6}} + decode_bin_rows(rest, fields, null_bitfield, [time | acc], :structs) + end + + defp decode_time(<< 0::8-little, rest::bits>>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{0, 0, 0, 0} | acc], :tuples) + end + + defp decode_time(<<8::8-little, _::8-little, _::32-little, hour::8-little, min::8-little, sec::8-little, rest::bits>>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{hour, min, sec, 0} | acc], :tuples) + end + + defp decode_time(<< 12::8, _::32-little, _::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do - decode_bin_rows(rest, fields, null_bitfield, [{hour, min, sec, msec} | acc]) + decode_bin_rows(rest, fields, null_bitfield, [{hour, min, sec, msec} | acc], :tuples) end defp decode_date(<< 0::8-little, rest::bits >>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{0, 0, 0} | acc]) + fields, null_bitfield, acc, :structs) do + date = %Date{year: 0, month: 1, day: 1} + decode_bin_rows(rest, fields, null_bitfield, [date | acc], :structs) end defp decode_date(<< 4::8-little, year::16-little, month::8-little, day::8-little, rest::bits >>, - fields, null_bitfield, acc) do + fields, null_bitfield, acc, :structs) do + date = %Date{year: year, month: month, day: day} + decode_bin_rows(rest, fields, null_bitfield, [date | acc], :structs) + end - decode_bin_rows(rest, fields, null_bitfield, [{year, month, day} | acc]) + defp decode_date(<< 0::8-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{0, 0, 0} | acc], :tuples) end + defp decode_date(<< 4::8-little, year::16-little, month::8-little, day::8-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + + decode_bin_rows(rest, fields, null_bitfield, [{year, month, day} | acc], :tuples) + end + + defp decode_datetime(<< 0::8-little, rest::bits >>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{{0, 0, 0}, {0, 0, 0, 0}} | acc]) + fields, null_bitfield, acc, :structs) do + datetime = %NaiveDateTime{year: 0, month: 1, day: 1, hour: 0, minute: 0, second: 0} + decode_bin_rows(rest, fields, null_bitfield, [datetime | acc], :structs) end defp decode_datetime(<<4::8-little, year::16-little, month::8-little, day::8-little, rest::bits >>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {0, 0, 0, 0}} | acc]) + fields, null_bitfield, acc, :structs) do + datetime = %NaiveDateTime{year: year, month: month, day: day, hour: 0, minute: 0, second: 0} + decode_bin_rows(rest, fields, null_bitfield, [datetime | acc], :structs) end defp decode_datetime(<< 7::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, rest::bits >>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {hour, min, sec, 0}} | acc]) + fields, null_bitfield, acc, :structs) do + datetime = %NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: min, second: sec} + decode_bin_rows(rest, fields, null_bitfield, [datetime | acc], :structs) end defp decode_datetime(<<11::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little, rest::bits >>, - fields, null_bitfield, acc) do - decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {hour, min, sec, msec}} | acc]) + fields, null_bitfield, acc, :structs) do + datetime = %NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: min, second: sec, microsecond: {msec, 6}} + decode_bin_rows(rest, fields, null_bitfield, [datetime | acc], :structs) + end + + defp decode_datetime(<< 0::8-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{{0, 0, 0}, {0, 0, 0, 0}} | acc], :tuples) + end + + defp decode_datetime(<<4::8-little, year::16-little, month::8-little, day::8-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {0, 0, 0, 0}} | acc], :tuples) + end + + defp decode_datetime(<< 7::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {hour, min, sec, 0}} | acc], :tuples) + end + + defp decode_datetime(<<11::8-little, year::16-little, month::8-little, day::8-little, hour::8-little, min::8-little, sec::8-little, msec::32-little, rest::bits >>, + fields, null_bitfield, acc, :tuples) do + decode_bin_rows(rest, fields, null_bitfield, [{{year, month, day}, {hour, min, sec, msec}} | acc], :tuples) end ### TEXT ROW PARSER @@ -280,76 +335,93 @@ defmodule Mariaex.RowParser do end end - def decode_text_rows(binary, fields) do - decode_text_part(binary, fields, []) + def decode_text_rows(binary, fields, datetime) do + decode_text_part(binary, fields, [], datetime) end ### IMPLEMENTATION - defp decode_text_part(<>, fields, acc) when len <= 250 do - decode_text_rows(string, rest, fields, acc) + defp decode_text_part(<>, fields, acc, datetime) when len <= 250 do + decode_text_rows(string, rest, fields, acc, datetime) end - defp decode_text_part(<<252::8, len::16-little, string::size(len)-binary, rest::bits>>, fields, acc) do - decode_text_rows(string, rest, fields, acc) + defp decode_text_part(<<252::8, len::16-little, string::size(len)-binary, rest::bits>>, fields, acc, datetime) do + decode_text_rows(string, rest, fields, acc, datetime) end - defp decode_text_part(<<253::8, len::24-little, string::size(len)-binary, rest::bits>>, fields, acc) do - decode_text_rows(string, rest, fields, acc) + defp decode_text_part(<<253::8, len::24-little, string::size(len)-binary, rest::bits>>, fields, acc, datetime) do + decode_text_rows(string, rest, fields, acc, datetime) end - defp decode_text_part(<<254::8, len::64-little, string::size(len)-binary, rest::bits>>, fields, acc) do - decode_text_rows(string, rest, fields, acc) + defp decode_text_part(<<254::8, len::64-little, string::size(len)-binary, rest::bits>>, fields, acc, datetime) do + decode_text_rows(string, rest, fields, acc, datetime) end - defp decode_text_part(<<>>, [], acc) do + defp decode_text_part(<<>>, [], acc, _datetime) do Enum.reverse(acc) end - defp decode_text_rows(string, rest, [:string | fields], acc) do - decode_text_part(rest, fields, [string | acc]) + defp decode_text_rows(string, rest, [:string | fields], acc, datetime) do + decode_text_part(rest, fields, [string | acc], datetime) end - defp decode_text_rows(string, rest, [type | fields], acc) + defp decode_text_rows(string, rest, [type | fields], acc, datetime) when type in [:uint8, :int8, :uint16, :int16, :uint32, :int32, :uint64, :int64] do - decode_text_part(rest, fields, [:erlang.binary_to_integer(string) | acc]) + decode_text_part(rest, fields, [:erlang.binary_to_integer(string) | acc], datetime) end - defp decode_text_rows(string, rest, [type | fields], acc) + defp decode_text_rows(string, rest, [type | fields], acc, datetime) when type in [:float32, :float64, :decimal] do - decode_text_part(rest, fields, [:erlang.binary_to_float(string) | acc]) + decode_text_part(rest, fields, [:erlang.binary_to_float(string) | acc], datetime) end - defp decode_text_rows(string, rest, [:bit | fields], acc) do - decode_text_part(rest, fields, [string | acc]) + defp decode_text_rows(string, rest, [:bit | fields], acc, datetime) do + decode_text_part(rest, fields, [string | acc], datetime) end - defp decode_text_rows(string, rest, [:time | fields], acc) do - decode_text_time(string, rest, fields, acc) + defp decode_text_rows(string, rest, [:time | fields], acc, datetime) do + decode_text_time(string, rest, fields, acc, datetime) end - defp decode_text_rows(string, rest, [:date | fields], acc) do - decode_text_date(string, rest, fields, acc) + defp decode_text_rows(string, rest, [:date | fields], acc, datetime) do + decode_text_date(string, rest, fields, acc, datetime) end - defp decode_text_rows(string, rest, [:datetime | fields], acc) do - decode_text_datetime(string, rest, fields, acc) + defp decode_text_rows(string, rest, [:datetime | fields], acc, datetime) do + decode_text_datetime(string, rest, fields, acc, datetime) end defmacrop to_int(value) do quote do: :erlang.binary_to_integer(unquote(value)) end - defp decode_text_date(<>, rest, fields, acc) do - decode_text_part(rest, fields, [{to_int(year), to_int(month), to_int(day)} | acc]) + defp decode_text_date(<>, rest, fields, acc, :structs) do + date = %Date{year: to_int(year), month: to_int(month), day: to_int(day)} + decode_text_part(rest, fields, [date | acc], :structs) + end + + defp decode_text_date(<>, rest, fields, acc, :tuples) do + decode_text_part(rest, fields, [{to_int(year), to_int(month), to_int(day)} | acc], :tuples) + end + + defp decode_text_time(<>, rest, fields, acc, :structs) do + time = %Time{hour: to_int(hour), minute: to_int(min), second: to_int(sec)} + decode_text_part(rest, fields, [time | acc], :structs) + end + + defp decode_text_time(<>, rest, fields, acc, :tuples) do + decode_text_part(rest, fields, [{to_int(hour), to_int(min), to_int(sec), 0} | acc], :tuples) end - defp decode_text_time(<>, rest, fields, acc) do - decode_text_part(rest, fields, [{to_int(hour), to_int(min), to_int(sec), 0} | acc]) + defp decode_text_datetime(<>, rest, fields, acc, :structs) do + datetime = %NaiveDateTime{year: to_int(year), month: to_int(month), day: to_int(day), + hour: to_int(hour), minute: to_int(min), second: to_int(sec)} + decode_text_part(rest, fields, [datetime | acc], :structs) end defp decode_text_datetime(<>, rest, fields, acc) do - decode_text_part(rest, fields, [{{to_int(year), to_int(month), to_int(day)}, {to_int(hour), to_int(min), to_int(sec), 0}} | acc]) + _::8-little, hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes>>, rest, fields, acc, :tuples) do + decode_text_part(rest, fields, [{{to_int(year), to_int(month), to_int(day)}, {to_int(hour), to_int(min), to_int(sec), 0}} | acc], :tuples) end end diff --git a/mix.exs b/mix.exs index 0e478dd..8edef9a 100644 --- a/mix.exs +++ b/mix.exs @@ -3,8 +3,8 @@ defmodule Mariaex.Mixfile do def project do [app: :mariaex, - version: "0.8.3", - elixir: "~> 1.2", + version: "0.9.0-dev", + elixir: "~> 1.3", deps: deps(), name: "Mariaex", source_url: "https://github.com/liveforeverx/mariaex", diff --git a/test/query_test.exs b/test/query_test.exs index 5bafe86..41ec725 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -2,9 +2,11 @@ defmodule QueryTest do use ExUnit.Case, async: true import Mariaex.TestHelper - setup do - opts = [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", cache_size: 2, backoff_type: :stop] - {:ok, pid} = Mariaex.Connection.start_link(opts) + @opts [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", cache_size: 2, backoff_type: :stop] + + setup context do + connection_opts = context[:connection_opts] || [] + {:ok, pid} = Mariaex.Connection.start_link(connection_opts ++ @opts) # remove all modes for this session to have the same behaviour on different versions of mysql/mariadb {:ok, _} = Mariaex.Connection.query(pid, "SET SESSION sql_mode = \"\";") {:ok, [pid: pid]} @@ -230,8 +232,8 @@ defmodule QueryTest do end test "encode and decode date", context do - date0 = {2010, 10, 17} - date1 = {0, 0, 0} + date0 = ~D[2010-10-17] + date1 = ~D[0000-01-01] table = "test_dates" sql = ~s{CREATE TABLE #{table} (id int, d date)} @@ -247,9 +249,84 @@ defmodule QueryTest do end test "encode and decode time", context do + time = ~T[19:27:30] + time_with_msec = ~T[10:14:16.23] + table = "test_times" + + sql = ~s{CREATE TABLE #{table} (id int, t1 time, t2 time)} + :ok = query(sql, []) + + insert = ~s{INSERT INTO #{table} (id, t1, t2) VALUES (?, ?, ?)} + :ok = query(insert, [1, time, time_with_msec]) + + # Time + # Only MySQL 5.7 supports microseconds storage, so it will return 0 here + assert query("SELECT t1, t2 FROM #{table} WHERE id = 1", []) == [[~T[19:27:30], ~T[10:14:16]]] + assert query("SELECT t1, t2 FROM #{table} WHERE id = ?", [1]) == [[~T[19:27:30], ~T[10:14:16]]] + assert query("SELECT time('00:00:00')", []) == [[~T[00:00:00]]] + end + + test "encode and decode datetime", context do + datetime = ~N[2010-10-17 10:10:30] + datetime_with_msec = ~N[2010-10-17 13:32:15.12] + table = "test_datetimes" + + sql = ~s{CREATE TABLE #{table} (id int, dt1 datetime, dt2 datetime)} + :ok = query(sql, []) + + insert = ~s{INSERT INTO #{table} (id, dt1, dt2) VALUES (?, ?, ?)} + :ok = query(insert, [1, datetime, datetime_with_msec]) + + # Datetime + # Only MySQL 5.7 supports microseconds storage, so it will return 0 here + assert query("SELECT dt1, dt2 FROM #{table} WHERE id = 1", []) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]] + assert query("SELECT dt1, dt2 FROM #{table} WHERE id = ?", [1]) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]] + end + + test "encode and decode timestamp", context do + timestamp = ~N[2010-10-17 10:10:30] + timestamp_with_msec = ~N[2010-10-17 13:32:15.12] + table = "test_timestamps" + + sql = ~s{CREATE TABLE #{table} (id int, ts1 timestamp, ts2 timestamp)} + :ok = query(sql, []) + + insert = ~s{INSERT INTO #{table} (id, ts1, ts2) VALUES (?, ?, ?)} + :ok = query(insert, [1, timestamp, timestamp_with_msec]) + + # Timestamp + # Only MySQL 5.7 supports microseconds storage, so it will return 0 here + assert query("SELECT ts1, ts2 FROM #{table} WHERE id = 1", []) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]] + assert query("SELECT ts1, ts2 FROM #{table} WHERE id = ?", [1]) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]] + assert query("SELECT timestamp('0000-00-00 00:00:00')", []) == [[~N[0000-01-01 00:00:00]]] + assert query("SELECT timestamp('0001-01-01 00:00:00')", []) == [[~N[0001-01-01 00:00:00]]] + assert query("SELECT timestamp('2013-12-21 23:01:27')", []) == [[~N[2013-12-21 23:01:27]]] + assert query("SELECT timestamp('2013-12-21 23:01:27 EST')", []) == [[~N[2013-12-21 23:01:27]]] + end + + @tag connection_opts: [datetime: :tuples] + test "encode and decode tuples date", context do + date0 = {2010, 10, 17} + date1 = {0, 0, 0} + table = "test_tuples_dates" + + sql = ~s{CREATE TABLE #{table} (id int, d date)} + :ok = query(sql, []) + + insert = ~s{INSERT INTO #{table} (id, d) VALUES (?, ?)} + :ok = query(insert, [1, date0]) + :ok = query(insert, [2, date1]) + + assert query("SELECT d FROM #{table} WHERE id = 1", []) == [[date0]] + assert query("SELECT d FROM #{table} WHERE id = ?", [1]) == [[date0]] + assert query("SELECT d FROM #{table} WHERE id = ?", [2]) == [[date1]] + end + + @tag connection_opts: [datetime: :tuples] + test "encode and decode tuples time", context do time = {19, 27, 30, 0} time_with_msec = {10, 14, 16, 23} - table = "test_times" + table = "test_tuples_times" sql = ~s{CREATE TABLE #{table} (id int, t1 time, t2 time)} :ok = query(sql, []) @@ -264,12 +341,13 @@ defmodule QueryTest do assert query("SELECT time('00:00:00')", []) == [[{0, 0, 0, 0}]] end - test "encode and decode datetime", context do + @tag connection_opts: [datetime: :tuples] + test "encode and decode tuples datetime", context do date = {2010, 10, 17} datetime = {date, {10, 10, 30, 0}} datetime_with_msec = {date, {13, 32, 15, 12}} datetime_no_msec = {date, {10, 10, 29}} - table = "test_datetimes" + table = "test_tuples_datetimes" sql = ~s{CREATE TABLE #{table} (id int, dt1 datetime, dt2 datetime, dt3 datetime)} :ok = query(sql, []) @@ -285,11 +363,12 @@ defmodule QueryTest do assert query("SELECT COUNT(*) FROM #{table} WHERE dt3 = ?", [datetime_no_msec]) == [[1]] end - test "encode and decode timestamp", context do + @tag connection_opts: [datetime: :tuples] + test "encode and decode tuples timestamp", context do date = {2010, 10, 17} timestamp = {date, {10, 10, 30, 0}} timestamp_with_msec = {date, {13, 32, 15, 12}} - table = "test_timestamps" + table = "test_tuples_timestamps" sql = ~s{CREATE TABLE #{table} (id int, ts1 timestamp, ts2 timestamp)} :ok = query(sql, []) diff --git a/test/text_query_test.exs b/test/text_query_test.exs index b53ecf0..1e1edc7 100644 --- a/test/text_query_test.exs +++ b/test/text_query_test.exs @@ -2,9 +2,10 @@ defmodule TextQueryTest do use ExUnit.Case, async: true import Mariaex.TestHelper + @opts [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", backoff_type: :stop] + setup_all do - opts = [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", backoff_type: :stop] - {:ok, pid} = Mariaex.Connection.start_link(opts) + {:ok, pid} = Mariaex.Connection.start_link(@opts) # drop = "DROP TABLE IF EXISTS test" # {:ok, _} = Mariaex.execute(pid, %Mariaex.Query{type: :text, statement: drop}, []) create = """ @@ -58,16 +59,28 @@ defmodule TextQueryTest do test "select timestamp", context do rows = execute_text("SELECT ts FROM test_text_query_table", []) - assert(rows == [[{{2016, 9, 26}, {16, 36, 06, 0}}], [{{2016, 9, 26}, {16, 36, 07, 0}}]]) + assert(rows == [[~N[2016-09-26 16:36:06]], [~N[2016-09-26 16:36:07]]]) end test "select datetime", context do rows = execute_text("SELECT dt FROM test_text_query_table", []) - assert(rows == [[{{1,1,1}, {0,0,0,0}}], [{{1,1,1}, {0,0,1,0}}]]) + assert(rows == [[~N[0001-01-01 00:00:00]], [~N[0001-01-01 00:00:01]]]) end test "select multiple columns", context do rows = execute_text("SELECT id, varchars FROM test_text_query_table", []) assert(rows == [[1, "hello"], [2, "goodbye"]]) end + + test "select tuple timestamp" do + {:ok, pid} = Mariaex.Connection.start_link([datetime: :tuples] ++ @opts) + {:ok, %{rows: rows}} = Mariaex.query(pid, "SELECT ts FROM test_text_query_table", [], query_type: :text) + assert(rows == [[{{2016, 9, 26}, {16, 36, 06, 0}}], [{{2016, 9, 26}, {16, 36, 07, 0}}]]) + end + + test "select tuple datetime" do + {:ok, pid} = Mariaex.Connection.start_link([datetime: :tuples] ++ @opts) + {:ok, %{rows: rows}} = Mariaex.query(pid, "SELECT dt FROM test_text_query_table", [], query_type: :text) + assert(rows == [[{{1,1,1}, {0,0,0,0}}], [{{1,1,1}, {0,0,1,0}}]]) + end end From a334e89abd58b8b7b8066435ec39448624f3123d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 2 Oct 2017 08:38:13 +0200 Subject: [PATCH 2/2] Improve coverage of datetime structs tests --- test/query_test.exs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/query_test.exs b/test/query_test.exs index 41ec725..e02894b 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -243,9 +243,15 @@ defmodule QueryTest do :ok = query(insert, [1, date0]) :ok = query(insert, [2, date1]) + # Strings + assert query("SELECT cast(d AS char) FROM #{table} WHERE id = 1", []) == [["2010-10-17"]] + assert query("SELECT cast(d AS char) FROM #{table} WHERE id = 2", []) == [["0000-01-01"]] + + # Date assert query("SELECT d FROM #{table} WHERE id = 1", []) == [[date0]] assert query("SELECT d FROM #{table} WHERE id = ?", [1]) == [[date0]] assert query("SELECT d FROM #{table} WHERE id = ?", [2]) == [[date1]] + assert query("SELECT date('0000-01-01')", []) == [[date1]] end test "encode and decode time", context do @@ -259,6 +265,10 @@ defmodule QueryTest do insert = ~s{INSERT INTO #{table} (id, t1, t2) VALUES (?, ?, ?)} :ok = query(insert, [1, time, time_with_msec]) + # Strings + assert query("SELECT cast(t1 as char), cast(t2 as char) FROM #{table} WHERE id = 1", []) == + [["19:27:30", "10:14:16"]] + # Time # Only MySQL 5.7 supports microseconds storage, so it will return 0 here assert query("SELECT t1, t2 FROM #{table} WHERE id = 1", []) == [[~T[19:27:30], ~T[10:14:16]]] @@ -277,6 +287,10 @@ defmodule QueryTest do insert = ~s{INSERT INTO #{table} (id, dt1, dt2) VALUES (?, ?, ?)} :ok = query(insert, [1, datetime, datetime_with_msec]) + # Strings + assert query("SELECT cast(dt1 as char), cast(dt2 as char) FROM #{table} WHERE id = 1", []) == + [["2010-10-17 10:10:30", "2010-10-17 13:32:15"]] + # Datetime # Only MySQL 5.7 supports microseconds storage, so it will return 0 here assert query("SELECT dt1, dt2 FROM #{table} WHERE id = 1", []) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]] @@ -294,6 +308,10 @@ defmodule QueryTest do insert = ~s{INSERT INTO #{table} (id, ts1, ts2) VALUES (?, ?, ?)} :ok = query(insert, [1, timestamp, timestamp_with_msec]) + # Strings + assert query("SELECT cast(ts1 as char), cast(ts2 as char) FROM #{table} WHERE id = 1", []) == + [["2010-10-17 10:10:30", "2010-10-17 13:32:15"]] + # Timestamp # Only MySQL 5.7 supports microseconds storage, so it will return 0 here assert query("SELECT ts1, ts2 FROM #{table} WHERE id = 1", []) == [[~N[2010-10-17 10:10:30], ~N[2010-10-17 13:32:15]]]