diff --git a/lib/mariaex.ex b/lib/mariaex.ex index 8e54222..938d115 100644 --- a/lib/mariaex.ex +++ b/lib/mariaex.ex @@ -16,10 +16,14 @@ defmodule Mariaex do """ @type conn :: DBConnection.conn - @pool_timeout 5000 - @timeout 5000 @idle_timeout 5000 @max_rows 500 + + # Inherited from DBConnection + + @pool_timeout 5000 + @timeout 15000 + ### PUBLIC API ### @doc """ @@ -61,8 +65,7 @@ defmodule Mariaex do * `:idle` - Either `:active` to asynchronously detect TCP disconnects when idle or `:passive` not to (default: `:passive`); * `:pool` - The pool module to use, see `DBConnection` for pool dependent - options, this option must be included with all requests contacting the pool - if not `DBConnection.Connection` (default: `DBConnection.Connection`); + options (default: `DBConnection.ConnectionPool`); * `: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 @@ -132,18 +135,17 @@ defmodule Mariaex do param_types: ["text", "text"], result_types: ["text"]) """ - @spec query(conn, iodata, list, Keyword.t) :: {:ok, Mariaex.Result.t} | {:error, Mariaex.Error.t} + @spec query(conn, iodata, list, Keyword.t) :: {:ok, Mariaex.Result.t} | {:error, Exception.t} def query(conn, statement, params \\ [], opts \\ []) def query(conn, statement, params, opts) do case Keyword.get(opts, :query_type) do :text -> - query = %Query{type: :text, statement: statement, ref: make_ref(), - num_params: 0} - execute(conn, query, [], opts) + query = %Query{type: :text, statement: statement, ref: make_ref(), num_params: 0} + run_query(:execute, conn, query, [], opts) type when type in [:binary, nil] -> query = %Query{type: type, statement: statement} - prepare_execute(conn, query, params, defaults(opts)) + run_query(:prepare_execute, conn, query, params, opts) end end @@ -173,8 +175,6 @@ defmodule Mariaex do (default: `#{@pool_timeout}`) * `:queue` - Whether to wait for connection in a queue (default: `true`); * `:timeout` - Prepare request timeout (default: `#{@timeout}`); - * `:pool` - The pool module to use, must match that set on - `start_link/1`, see `DBConnection` * `:query_type` - `:binary` to use binary protocol, `:text` to use text protocol or `nil` to try binary but fallback to text (default `nil`) @@ -182,7 +182,7 @@ defmodule Mariaex do Mariaex.prepare(conn, "", "CREATE TABLE posts (id serial, title text)") """ - @spec prepare(conn, iodata, iodata, Keyword.t) :: {:ok, Mariaex.Query.t} | {:error, Mariaex.Error.t} + @spec prepare(conn, iodata, iodata, Keyword.t) :: {:ok, Mariaex.Query.t} | {:error, Exception.t} def prepare(conn, name, statement, opts \\ []) do case Keyword.get(opts, :query_type) do :text -> @@ -191,7 +191,7 @@ defmodule Mariaex do {:ok, query} type when type in [:binary, nil] -> query = %Query{type: type, name: name, statement: statement} - prepare_binary(conn, query, opts) + DBConnection.prepare(conn, query, opts) end end @@ -209,10 +209,64 @@ defmodule Mariaex do end end + @doc """ + Prepares and executes a query and returns the result as + `{:ok, %Mariaex.Query{}, %Mariaex.Result{}}` or `{:error, exception}` + if there was an error. Parameters are given as part of the prepared query, + `%Mariaex.Query{}`. + + See the README for information on how Mariaex encodes and decodes Elixir + values by default. See `Mariaex.Query` for the query data and + `Mariaex.Result` for the result data. + + ## Options + + * `:pool_timeout` - Time to wait in the queue for the connection + (default: `#{@pool_timeout}`) + * `:queue` - Whether to wait for connection in a queue (default: `true`); + * `:timeout` - Execute request timeout (default: `#{@timeout}`); + * `:decode_mapper` - Fun to map each row in the result to a term after + decoding, (default: `fn x -> x end`); + + ## Examples + + Mariaex.prepare_execute(conn, "", "SELECT id FROM posts WHERE title like $1", ["%my%"]) + + """ + @spec prepare_execute(conn, iodata, iodata, list, Keyword.t) :: + {:ok, Mariaex.Query.t, Mariaex.Result.t} | {:error, Exception.t} + def prepare_execute(conn, name, statement, params, opts \\ []) do + case Keyword.get(opts, :query_type) do + :text -> + query = %Query{type: :text, name: name, statement: statement, ref: make_ref(), num_params: 0} + DBConnection.execute(conn, query, params, opts) + type when type in [:binary, nil] -> + query = %Query{type: type, name: name, statement: statement} + DBConnection.prepare_execute(conn, query, params, opts) + end + end + + @doc """ + Prepares and executes a query and returns the result or raises + `Mariaex.Error` if there was an error. See `execute/4`. + """ + @spec prepare_execute!(conn, Mariaex.Query.t, list, Keyword.t) :: + {Mariaex.Query.t, Mariaex.Result.t} + def prepare_execute!(conn, name, statement, params, opts \\ []) do + case prepare_execute(conn, name, statement, params, opts) do + {:ok, query, result} -> + {query, result} + {:error, err} -> + raise err + end + end + @doc """ Runs an (extended) prepared query and returns the result as - `{:ok, %Mariaex.Result{}}` or `{:error, %Mariaex.Error{}}` if there was an - error. Parameters are given as part of the prepared query, `%Mariaex.Query{}`. + `{:ok, %Mariaex.Query{}, %Mariaex.Result{}}` or `{:error, %Mariaex.Error{}}` + if there was an error. Parameters are given as part of the prepared query, + `%Mariaex.Query{}`. + See the README for information on how Mariaex encodes and decodes Elixir values by default. See `Mariaex.Query` for the query data and `Mariaex.Result` for the result data. @@ -225,8 +279,6 @@ defmodule Mariaex do * `:timeout` - Execute request timeout (default: `#{@timeout}`); * `:decode_mapper` - Fun to map each row in the result to a term after decoding, (default: `fn x -> x end`); - * `:pool` - The pool module to use, must match that set on - `start_link/1`, see `DBConnection` ## Examples @@ -237,14 +289,9 @@ defmodule Mariaex do Mariaex.execute(conn, query, ["%my%"]) """ @spec execute(conn, Mariaex.Query.t, list, Keyword.t) :: - {:ok, Mariaex.Result.t} | {:error, Mariaex.Error.t} + {:ok, Mariaex.Query.t, Mariaex.Result.t} | {:error, Exception.t} def execute(conn, %Query{} = query, params, opts \\ []) do - case DBConnection.execute(conn, query, params, defaults(opts)) do - {:error, %ArgumentError{} = err} -> - raise err - other -> - other - end + DBConnection.execute(conn, query, params, opts) end @doc """ @@ -253,12 +300,7 @@ defmodule Mariaex do """ @spec execute!(conn, Mariaex.Query.t, list, Keyword.t) :: Mariaex.Result.t def execute!(conn, query, params, opts \\ []) do - case execute(conn, query, params, opts) do - {:ok, res} -> - res - {:error, err} -> - raise err - end + DBConnection.execute!(conn, query, params, opts) end @doc """ @@ -271,28 +313,24 @@ defmodule Mariaex do (default: `#{@pool_timeout}`) * `:queue` - Whether to wait for connection in a queue (default: `true`); * `:timeout` - Prepare request timeout (default: `#{@timeout}`); - * `:pool` - The pool module to use, must match that set on - `start_link/1`, see `DBConnection` ## Examples query = Mariaex.prepare!(conn, "SELECT id FROM posts WHERE title like $1") Mariaex.close(conn, query) """ - @spec close(conn, Mariaex.Query.t, Keyword.t) :: :ok | {:error, Mariaex.Error.t} + @spec close(conn, Mariaex.Query.t, Keyword.t) :: :ok | {:error, Exception.t} def close(conn, query, opts \\ []) def close(_, %Query{type: :text}, _) do :ok end def close(conn, query, opts) do - case DBConnection.close(conn, query, defaults(opts)) do + case DBConnection.close(conn, query, opts) do {:ok, _} -> :ok - {:error, %ArgumentError{} = err} -> - raise err - other -> - other + {:error, _} = error -> + error end end @@ -333,16 +371,13 @@ defmodule Mariaex do (default: `#{@pool_timeout}`) * `:queue` - Whether to wait for connection in a queue (default: `true`); * `:timeout` - Transaction timeout (default: `#{@timeout}`); - * `:pool` - The pool module to use, must match that set on - `start_link/1`, see `DBConnection; * `:mode` - Set to `:savepoint` to use savepoints instead of an SQL transaction, otherwise set to `:transaction` (default: `:transaction`); - The `:timeout` is for the duration of the transaction and all nested transactions and requests. This timeout overrides timeouts set by internal - transactions and requests. The `:pool` and `:mode` will be used for all - requests inside the transaction function. + transactions and requests. The `:mode` will be used for all requests inside + the transaction function. ## Example @@ -353,7 +388,7 @@ defmodule Mariaex do @spec transaction(conn, ((DBConnection.t) -> result), Keyword.t) :: {:ok, result} | {:error, any} when result: var def transaction(conn, fun, opts \\ []) do - DBConnection.transaction(conn, fun, defaults(opts)) + DBConnection.transaction(conn, fun, opts) end @doc """ @@ -393,7 +428,7 @@ defmodule Mariaex do def stream(conn, query, params, opts \\ []) def stream(conn, %Query{} = query, params, opts) do - DBConnection.stream(conn, query, params, defaults(opts)) + DBConnection.stream(conn, query, params, opts) end def stream(conn, statement, params, opts) do case Keyword.get(opts, :query_type) do @@ -403,7 +438,7 @@ defmodule Mariaex do stream(conn, query, [], opts) type when type in [:binary, nil] -> query = %Query{type: type, statement: statement} - prepare_stream(conn, query, params, opts) + DBConnection.prepare_stream(conn, query, params, opts) end end @@ -417,31 +452,12 @@ defmodule Mariaex do ## Helpers - defp prepare_execute(conn, query, params, opts) do - case DBConnection.prepare_execute(conn, query, params, defaults(opts)) do + defp run_query(op, conn, query, params, opts) do + case apply(DBConnection, op, [conn, query, params, opts]) do {:ok, _, result} -> {:ok, result} - {:error, %ArgumentError{} = err} -> - raise err {:error, _} = error -> error end end - - defp prepare_binary(conn, query, opts) do - case DBConnection.prepare(conn, query, defaults(opts)) do - {:error, %ArgumentError{} = err} -> - raise err - other -> - other - end - end - - defp prepare_stream(conn, query, params, opts) do - DBConnection.prepare_stream(conn, query, params, defaults(opts)) - end - - defp defaults(opts) do - Keyword.put_new(opts, :timeout, @timeout) - end end diff --git a/lib/mariaex/protocol.ex b/lib/mariaex/protocol.ex index 9ab89ea..a1c82b8 100644 --- a/lib/mariaex/protocol.ex +++ b/lib/mariaex/protocol.ex @@ -12,10 +12,10 @@ defmodule Mariaex.Protocol do use DBConnection use Bitwise - @reserved_prefix "MARIAEX_" @timeout 5000 @cache_size 100 @max_rows 500 + @nonposix_errors [:closed, :timeout] @maxpacketbytes 50000000 @mysql_native_password "mysql_native_password" @@ -35,6 +35,7 @@ defmodule Mariaex.Protocol do @client_ps_multi_results 0x00040000 @client_deprecate_eof 0x01000000 + @server_status_in_trans 0x0001 @server_more_results_exists 0x0008 @server_status_cursor_exists 0x0040 @server_status_last_row_sent 0x0080 @@ -63,6 +64,7 @@ defmodule Mariaex.Protocol do seqnum: 0, datetime: :structs, json_library: Poison, + transaction_status: :idle, ssl_conn_state: :undefined # :undefined | :not_used | :ssl_handshake | :connected @doc """ @@ -222,7 +224,7 @@ defmodule Mariaex.Protocol do case send_text_query(state, statement) |> text_query_recv(query) do {:error, error, _} -> {:error, error} - {:ok, _, state} -> + {:ok, _, _, state} -> activate(state, state.buffer) |> connected() end end @@ -324,9 +326,6 @@ defmodule Mariaex.Protocol do @doc """ DBConnection callback """ - def handle_prepare(%Query{name: @reserved_prefix <> _} = query, _, s) do - reserved_error(query, s) - end def handle_prepare(%Query{type: nil} = query, opts, s) do case handle_prepare(%Query{query | type: :binary}, opts, s) do {:error, %Mariaex.Error{mariadb: %{code: 1295}}, s} -> @@ -383,8 +382,8 @@ defmodule Mariaex.Protocol do defp prepare_recv(state, query) do case prepare_recv(state) do - {:prepared, id, num_params, state} -> - {:ok, prepare_insert(id, num_params, query, state), clean_state(state)} + {:prepared, id, num_params, flags, state} -> + {:ok, prepare_insert(id, num_params, query, state), clean_state(state, flags)} {:ok, packet, state} -> handle_error(packet, query, state) {:error, reason} -> @@ -395,13 +394,13 @@ defmodule Mariaex.Protocol do defp prepare_recv(state) do state = %{state | state: :prepare_send} with {:ok, packet(msg: stmt_prepare_ok(statement_id: id, num_columns: num_cols, num_params: num_params)), state} <- msg_recv(state), - {:eof, state} <- skip_definitions(state, num_params), - {:eof, state} <- skip_definitions(state, num_cols) do - {:prepared, id, num_params, state} + {:eof, _, state} <- skip_definitions(state, num_params), + {:eof, flags, state} <- skip_definitions(state, num_cols) do + {:prepared, id, num_params, flags, state} end end - defp skip_definitions(state, 0), do: {:eof, state} + defp skip_definitions(state, 0), do: {:eof, nil, state} defp skip_definitions(state, count) do do_skip_definitions(%{state | state: :column_definitions}, count) end @@ -415,12 +414,12 @@ defmodule Mariaex.Protocol do end end defp do_skip_definitions(%{deprecated_eof: true} = state, 0) do - {:eof, state} + {:eof, nil, state} end defp do_skip_definitions(%{deprecated_eof: false} = state, 0) do case msg_recv(state) do - {:ok, packet(msg: eof_resp()), state} -> - {:eof, state} + {:ok, packet(msg: eof_resp(status_flags: flags)), state} -> + {:eof, flags, state} other -> other end @@ -440,9 +439,6 @@ defmodule Mariaex.Protocol do @doc """ DBConnection callback """ - def handle_execute(%Query{name: @reserved_prefix <> _, reserved?: false} = query, _, s) do - reserved_error(query, s) - end def handle_execute(%Query{type: :text, statement: statement} = query, [], _opts, state) do send_text_query(state, statement) |> text_query_recv(query) end @@ -507,11 +503,12 @@ defmodule Mariaex.Protocol do defp text_query_recv(state, query) do case text_query_recv(state) do - {:resultset, columns, rows, _flags, state} -> + {:resultset, columns, rows, flags, state} -> result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} - {:ok, {result, columns}, clean_state(state)} + {:ok, query, {result, columns}, clean_state(state, flags)} {:ok, packet(msg: ok_resp()) = packet, state} -> - handle_ok_packet(packet, query, state) + {:ok, result, state} = handle_ok_packet(packet, query, state) + {:ok, query, result, state} {:ok, packet, state} -> handle_error(packet, query, state) {:error, reason} -> @@ -570,7 +567,8 @@ defmodule Mariaex.Protocol do {:resultset, columns, bin_rows, flags, state} -> binary_query_resultset(state, query, columns, bin_rows, flags) {:ok, packet(msg: ok_resp()) = packet, state} -> - handle_ok_packet(packet, query, state) + {:ok, result, state} = handle_ok_packet(packet, query, state) + {:ok, query, result, state} {:ok, packet, state} -> handle_error(packet, query, state) {:error, reason} -> @@ -630,16 +628,16 @@ defmodule Mariaex.Protocol do binary_query_more(state, query, columns, rows) true -> result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} - {:ok, {result, columns}, clean_state(state)} + {:ok, query, {result, columns}, clean_state(state, flags)} end end defp binary_query_more(state, query, columns, rows) do case msg_recv(state) do - {:ok, packet(msg: ok_resp(affected_rows: affected_rows, last_insert_id: last_insert_id)), state} -> + {:ok, packet(msg: ok_resp(affected_rows: affected_rows, last_insert_id: last_insert_id, status_flags: flags)), state} -> result = %Mariaex.Result{rows: rows, num_rows: affected_rows, last_insert_id: last_insert_id, connection_id: state.connection_id} - {:ok, {result, columns}, clean_state(state)} + {:ok, query, {result, columns}, clean_state(state, flags)} {:ok, packet, state} -> handle_error(packet, query, state) {:error, reason} -> @@ -668,22 +666,45 @@ defmodule Mariaex.Protocol do end end - defp handle_ok_packet(packet(msg: ok_resp(affected_rows: affected_rows, last_insert_id: last_insert_id)), _query, s) do + defp handle_ok_packet(packet(msg: ok_resp(affected_rows: affected_rows, last_insert_id: last_insert_id, status_flags: flags)), _query, s) do result = %Mariaex.Result{columns: [], rows: nil, num_rows: affected_rows, last_insert_id: last_insert_id, connection_id: s.connection_id} - {:ok, {result, nil}, clean_state(s)} + {:ok, {result, nil}, clean_state(s, flags)} + end + + defp clean_state(state, flags) do + status = transaction_status(state, flags) + state = %{state | state: :running, state_data: nil, transaction_status: status} + case status do + :idle -> + clean_cursors(state) + :transaction -> + state + end + end + + defp transaction_status(_, flags) when is_integer(flags) do + case flags &&& @server_status_in_trans do + @server_status_in_trans -> + :transaction + 0 -> + :idle + end + end + defp transaction_status(%{transaction_status: status}, nil) do + status end - defp clean_state(state) do - %{state | state: :running, state_data: nil} + defp clean_cursors(%{cursors: cursors} = state) do + for {_ref, {_status, id, _info}} <- cursors, is_integer(id) do + msg_send(stmt_close(command: com_stmt_close(), statement_id: id), state, 0) + end + %{state | cursors: %{}} end @doc """ DBConnection callback """ - def handle_close(%Query{name: @reserved_prefix <> _ , reserved?: false} = query, _, s) do - reserved_error(query, s) - end def handle_close(%Query{type: :text}, _, s) do {:ok, nil, s} end @@ -714,17 +735,18 @@ defmodule Mariaex.Protocol do end end - def handle_declare(query, params, opts, state) do + def handle_declare(query, params, _, state) do case declare_lookup(query, state) do {:declare, id} -> - declare(id, params, opts, state) + cursor = %Cursor{statement_id: id, ref: make_ref()} + declare(cursor, query, params, state) {:prepare_declare, query} -> - prepare_declare(&prepare(query, &1), params, opts, state) + prepare_declare(&prepare(query, &1), params, state) {:close_prepare_declare, id, query} -> - prepare_declare(&close_prepare(id, query, &1), params, opts, state) + prepare_declare(&close_prepare(id, query, &1), params, state) {:text, _} -> - cursor = %Cursor{statement_id: :text, params: params, ref: make_ref()} - {:ok, cursor, state} + cursor = %Cursor{statement_id: :text, ref: make_ref()} + declare(cursor, query, params, state) end end @@ -750,17 +772,18 @@ defmodule Mariaex.Protocol do end end - defp declare(id, params, opts, state) do - max_rows = Keyword.get(opts, :max_rows, @max_rows) - cursor = %Cursor{statement_id: id, params: params, ref: make_ref(), max_rows: max_rows} - {:ok, cursor, state} + defp declare(%Cursor{ref: ref, statement_id: id} = cursor, query, params, state) do + state = put_in(state.cursors[ref], {:first, id, params}) + # close cursor if idle + {:ok, query, cursor, clean_state(state, nil)} end - defp prepare_declare(prepare, params, opts, state) do + defp prepare_declare(prepare, params, state) do case prepare.(state) do {:ok, query, state} -> id = prepare_declare_lookup(query, state) - declare(id, params, opts, state) + cursor = %Cursor{statement_id: id, ref: make_ref()} + declare(cursor, query, params, state) {err, _, _} = error when err in [:error, :disconnect] -> error end @@ -773,28 +796,59 @@ defmodule Mariaex.Protocol do Cache.take(cache, name) end - def handle_first(query, %Cursor{statement_id: :text, params: params}, opts, state) do + def handle_fetch(query, cursor, opts, state) do + %Cursor{ref: ref, statement_id: id} = cursor + %{cursors: cursors} = state + case cursors do + %{^ref => {:first, _, params}} -> + first(query, cursor, params, opts, state) |> fetch_result(ref, id) + %{^ref => {:cont, _, columns}} -> + next(query, cursor, columns, opts, state) |> fetch_result(ref, id) + %{^ref => {:halt, _, columns}} -> + # cursor finished, empty result + result = %Mariaex.Result{rows: [], num_rows: 0} + {:halt, {result, columns}, state} + %{} -> + msg = "could not find active cursor: #{inspect cursor}" + {:error, Mariaex.Error.exception(msg), state} + end + end + + defp fetch_result({:cont, {_, columns} = res, state}, ref, id) do + {:cont, res, put_in(state.cursors[ref], {:cont, id, columns})} + end + defp fetch_result({:halt, {_, columns} = res, state}, ref, id) do + {:halt, res, put_in(state.cursors[ref], {:halt, id, columns})} + end + defp fetch_result({:error, _, _} = error, _ref, _id) do + error + end + defp fetch_result({:disconnect, _, _} = disconnect, _ref, _id) do + disconnect + end + + defp first(query, %Cursor{statement_id: :text}, params, opts, state) do case handle_execute(query, params, opts, state) do - {:ok, result, state} -> - {:deallocate, result, state} + {:ok, _, result, state} -> + {:halt, result, state} other -> other end end - def handle_first(query, %Cursor{statement_id: id, ref: ref, params: params}, _, state) do + defp first(query, %Cursor{statement_id: id}, params, _, state) do msg_send(stmt_execute(command: com_stmt_execute(), parameters: params, statement_id: id, flags: @cursor_type_read_only, iteration_count: 1), state, 0) - binary_first_recv(state, ref, query) + binary_first_recv(state, query) end - defp binary_first_recv(state, ref, query) do + defp binary_first_recv(state, query) do case binary_first_recv(state) do {:eof, columns, flags, state} -> - binary_first_resultset(state, query, ref, columns, [], flags) + binary_first_resultset(state, query, columns, [], flags) {:resultset, columns, rows, flags, state} -> - binary_first_resultset(state, query, ref, columns, rows, flags) + binary_first_resultset(state, query, columns, rows, flags) {:ok, packet(msg: ok_resp()) = packet, state} -> {:ok, result, state} = handle_ok_packet(packet, query, state) - {:deallocate, result, state} + {:halt, result, state} {:ok, packet, state} -> handle_error(packet, query, state) {:error, reason} -> @@ -811,35 +865,35 @@ defmodule Mariaex.Protocol do end end - defp binary_first_resultset(state, query, ref, columns, rows, flags) do + defp binary_first_resultset(state, query, columns, rows, flags) do cond do (flags &&& @server_more_results_exists) == @server_more_results_exists -> binary_first_more(state, query, columns, rows) (flags &&& @server_status_cursor_exists) == @server_status_cursor_exists -> - %{cursors: cursors} = state - state = clean_state(%{state | cursors: Map.put(cursors, ref, columns)}) - {:ok, {%Mariaex.Result{rows: rows, connection_id: state.connection_id}, columns}, state} + result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} + {:cont, {result, columns}, clean_state(state, flags)} true -> - {:deallocate, {%Mariaex.Result{rows: rows, connection_id: state.connection_id}, columns}, clean_state(state)} + result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} + {:halt, {result, columns}, clean_state(state, flags)} end end defp binary_first_more(state, query, columns, rows) do case binary_query_more(state, query, columns, rows) do - {:ok, res, state} -> - {:deallocate, res, state} + {:ok, _query, res, state} -> + {:halt, res, state} other -> other end end - def handle_next(query, %Cursor{statement_id: id, ref: ref, max_rows: max_rows}, _, state) do + defp next(query, %Cursor{statement_id: id}, columns, opts, state) do + max_rows = Keyword.get(opts, :max_rows, @max_rows) msg_send(stmt_fetch(command: com_stmt_fetch(), statement_id: id, num_rows: max_rows), state, 0) - binary_next_recv(state, ref, query) + binary_next_recv(state, query, columns) end - defp binary_next_recv(%{cursors: cursors} = state, ref, query) do - columns = Map.fetch!(cursors, ref) + defp binary_next_recv(state, query, columns) do case bin_rows_recv(state, columns) do {:eof, rows, flags, state} -> binary_next_resultset(state, columns, rows, flags) @@ -853,18 +907,24 @@ defmodule Mariaex.Protocol do defp binary_next_resultset(state, columns, rows, flags) do cond do (flags &&& @server_status_last_row_sent) == @server_status_last_row_sent -> - {:deallocate, {%Mariaex.Result{rows: rows, connection_id: state.connection_id}, columns}, clean_state(state)} + result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} + {:halt, {result, columns}, clean_state(state, flags)} (flags &&& @server_status_cursor_exists) == @server_status_cursor_exists -> - {:ok, {%Mariaex.Result{rows: rows, connection_id: state.connection_id}, columns}, clean_state(state)} + result = %Mariaex.Result{rows: rows, connection_id: state.connection_id} + {:cont, {result, columns}, clean_state(state, flags)} end end - def handle_deallocate(_, %Cursor{statement_id: :text}, _, state) do - {:ok, nil, state} - end - def handle_deallocate(query, %Cursor{statement_id: id, ref: ref}, _, state) do - %{cursors: cursors} = state - deallocate(id, query, %{state | cursors: Map.delete(cursors, ref)}) + def handle_deallocate(query, cursor, _, state) do + %Cursor{ref: ref, statement_id: id} = cursor + case pop_in(state.cursors[ref]) do + {nil, state} -> + {:ok, nil, state} + {_exists, state} when id == :text -> + {:ok, nil, state} + {_exists, state} -> + deallocate(id, query, state) + end end defp deallocate(id, query, state) do @@ -916,64 +976,77 @@ defmodule Mariaex.Protocol do @doc """ DBConnection callback """ - def handle_begin(opts, s) do + def handle_begin(opts, %{transaction_status: status} = s) do case Keyword.get(opts, :mode, :transaction) do - :transaction -> - name = @reserved_prefix <> "BEGIN" - handle_transaction(name, :begin, opts, s) - :savepoint -> - name = @reserved_prefix <> "SAVEPOINT mariaex_savepoint" - handle_savepoint([name], [:savepoint], opts, s) + :transaction when status == :idle -> + handle_transaction("BEGIN", s) + :savepoint when status == :transaction -> + handle_transaction("SAVEPOINT mariaex_savepoint", s) + mode when mode in [:transaction, :savepoint] -> + {status, s} end end @doc """ DBConnection callback """ - def handle_commit(opts, s) do + def handle_commit(opts, %{transaction_status: status} = s) do case Keyword.get(opts, :mode, :transaction) do - :transaction -> - name = @reserved_prefix <> "COMMIT" - handle_transaction(name, :commit, opts, s) - :savepoint -> - name = @reserved_prefix <> "RELEASE SAVEPOINT mariaex_savepoint" - handle_savepoint([name], [:release], opts, s) + :transaction when status == :transaction -> + handle_transaction("COMMIT", s) + :savepoint when status == :transaction -> + handle_transaction("RELEASE SAVEPOINT mariaex_savepoint", s) + mode when mode in [:transaction, :savepoint] -> + {status, s} end end @doc """ DBConnection callback """ - def handle_rollback(opts, s) do + def handle_rollback(opts, %{transaction_status: status} = s) do case Keyword.get(opts, :mode, :transaction) do - :transaction -> - name = @reserved_prefix <> "ROLLBACK" - handle_transaction(name, :rollback, opts, s) - :savepoint -> - names = [@reserved_prefix <> "ROLLBACK TO SAVEPOINT mariaex_savepoint", - @reserved_prefix <> "RELEASE SAVEPOINT mariaex_savepoint"] - handle_savepoint(names, [:rollback, :release], opts, s) + :transaction when status == :transaction -> + handle_transaction("ROLLBACK", s) + :savepoint when status == :transaction -> + rollback_release = + "ROLLBACK TO SAVEPOINT mariaex_savepoint; RELEASE SAVEPOINT mariaex_savepoint" + handle_transaction(rollback_release, s) + mode when mode in [:transaction, :savepoint] -> + {status, s} end end - defp handle_transaction(name, cmd, opts, state) do - query = %Query{type: :text, name: name, statement: to_string(cmd), reserved?: true} - handle_execute(query, [], opts, state) + @doc """ + DBConnection callback + """ + def handle_status(_, %{transaction_status: status} = state) do + {status, state} + end + + defp handle_transaction(statement, state) do + state + |> send_text_query(statement) + |> transaction_recv() end - defp handle_savepoint(names, cmds, opts, state) do - Enum.zip(names, cmds) |> Enum.reduce({:ok, nil, state}, - fn({@reserved_prefix <> name, _cmd}, {:ok, _, state}) -> - query = %Query{type: :text, name: @reserved_prefix <> name, statement: name} - case handle_execute(query, [], opts, state) do - {:ok, res, state} -> - {:ok, res, state} - other -> - other - end - ({_name, _cmd}, {:error, _, _} = error) -> - error - end) + defp transaction_recv(state) do + case msg_recv(state) do + {:ok, packet(msg: ok_resp(status_flags: flags)), state} + when (flags &&& @server_more_results_exists) == @server_more_results_exists -> + # rollback/release has multiple results + transaction_recv(state) + {:ok, packet(msg: ok_resp(status_flags: flags)), state} -> + result = %Mariaex.Result{columns: [], rows: nil, num_rows: 0, + last_insert_id: 0} + {:ok, result, clean_state(state, flags)} + {:ok, packet(msg: error_resp(error_code: code, error_message: message)), state} -> + err = %Mariaex.Error{mariadb: %{code: code, message: message}} + # connection in bad state and unlikely to recover + {:disconnect, err, state} + {:error, reason} -> + recv_error(reason, state) + end end defp recv_error(reason, %{sock: {sock_mod, _}} = state) do @@ -984,8 +1057,18 @@ defmodule Mariaex.Protocol do Do disconnect """ def do_disconnect(s, {tag, action, reason, buffer}) do - err = Mariaex.Error.exception(tag: tag, action: action, reason: reason) - do_disconnect(s, err, buffer) + msg = "#{tag} #{action}: #{format_error(tag, reason)}" + {:disconnect, DBConnection.ConnectionError.exception(msg), %{s | buffer: buffer}} + end + + defp format_error(_, reason) when reason in @nonposix_errors do + Atom.to_string(reason) + end + defp format_error(:tcp, reason) do + "#{:inet.format_error(reason)} - #{inspect(reason)}" + end + defp format_error(:ssl, reason) do + "#{:ssl.format_error(reason)} - #{inspect(reason)}" end defp do_disconnect(%{connection_id: connection_id} = state, %Mariaex.Error{} = err, buffer) do @@ -1162,14 +1245,9 @@ defmodule Mariaex.Protocol do case query do %Query{} -> {:ok, nil, s} = handle_close(query, [], s) - {:error, error, clean_state(s)} + {:error, error, clean_state(s, nil)} nil -> - {:error, error, clean_state(s)} + {:error, error, clean_state(s, nil)} end end - - defp reserved_error(query, s) do - error = ArgumentError.exception("query #{inspect query} uses reserved name") - {:error, error, s} - end end diff --git a/lib/mariaex/structs.ex b/lib/mariaex/structs.ex index d7fe510..cea7583 100644 --- a/lib/mariaex/structs.ex +++ b/lib/mariaex/structs.ex @@ -35,5 +35,5 @@ end defmodule Mariaex.Cursor do @moduledoc false - defstruct [:ref, :statement_id, :params, max_rows: 0] + defstruct [:ref, :statement_id] end diff --git a/mix.exs b/mix.exs index d97635a..913a2ee 100644 --- a/mix.exs +++ b/mix.exs @@ -24,8 +24,8 @@ defmodule Mariaex.Mixfile do end defp deps do - [{:decimal, "~> 1.0"}, - {:db_connection, "~> 1.1"}, + [{:decimal, "~> 1.2"}, + {:db_connection, "~> 2.0.0-dev", github: "elixir-ecto/db_connection", ref: "bb29b5c"}, {:coverex, "~> 1.4.10", only: :test}, {:ex_doc, ">= 0.0.0", only: :dev}, {:poison, ">= 0.0.0", optional: true}] diff --git a/mix.lock b/mix.lock index 8c88065..b5d8118 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,9 @@ -%{"certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm"}, +%{ + "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "coverex": {:hex, :coverex, "1.4.10", "f6b68f95b3d51d04571a09dd2071c980e8398a38cf663db22b903ecad1083d51", [:mix], [{:httpoison, "~> 0.9", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "db_connection": {:hex, :db_connection, "1.1.0", "b2b88db6d7d12f99997b584d09fad98e560b817a20dab6a526830e339f54cdb3", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.1.0", "3333732f17a90ff3057d7ab8c65f0930ca2d67e15cca812a91ead5633ed472fe", [:mix], [], "hexpm"}, + "db_connection": {:git, "https://github.com/elixir-ecto/db_connection.git", "bb29b5cdd2b6759d5253b316fc907aa62077cce1", [ref: "bb29b5c"]}, + "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "1.2.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, @@ -11,4 +12,5 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}} + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, +} diff --git a/test/mariaex/protocol_test.exs b/test/mariaex/protocol_test.exs deleted file mode 100644 index e7201a1..0000000 --- a/test/mariaex/protocol_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -defmodule Mariaex.ProtocolTest do - use ExUnit.Case - import ExUnit.CaptureLog - - describe "Integration test" do - test "Expect to disconnect from the database when it goes down" do - Process.flag(:trap_exit, true) - opts = [database: "information_schema", username: "mariaex_user", password: "mariaex_pass", backoff_type: :stop, sync_connect: true] - - log = capture_log(fn -> - assert {:ok, pid} = Mariaex.start_link(opts) - # Restart the docker container to assure that mariadb will stop - # responding to ping(s) - System.cmd("docker", ["restart", "mariadb"]) - end) - - # Check if we disconnected - assert log =~ "disconnected" - end - end -end diff --git a/test/prepared_query_test.exs b/test/prepared_query_test.exs index 63b57fb..e9f1b67 100644 --- a/test/prepared_query_test.exs +++ b/test/prepared_query_test.exs @@ -28,7 +28,14 @@ defmodule PreparedQueryTest do assert [[42]] = query("SELECT 42", []) end - test "prepare query and execute different queries with same name", context do + test "prepare_execute, execute and close", context do + assert {%Mariaex.Query{} = query, [[42]]} = prepare_execute("42", "SELECT ?", [42]) + assert [[41]] = execute(query, [41]) + assert :ok = close(query) + assert [[43]] = query("SELECT ?", [43]) + end + + test "prepare and execute different queries with same name", context do query42 = prepare("select", "SELECT 42") assert close(query42) == :ok assert %Mariaex.Query{} = prepare("select", "SELECT 41") diff --git a/test/query_test.exs b/test/query_test.exs index 4eda2c0..c998b2f 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -1,8 +1,9 @@ defmodule QueryTest do use ExUnit.Case, async: true import Mariaex.TestHelper + import ExUnit.CaptureLog - @opts [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", cache_size: 2, backoff_type: :stop] + @opts [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", cache_size: 2, backoff_type: :stop, max_restarts: 0] setup context do connection_opts = context[:connection_opts] || [] @@ -36,13 +37,14 @@ defmodule QueryTest do end test "queries are dequeued after previous query is processed", context do + Process.flag(:trap_exit, true) conn = context[:pid] - Process.flag(:trap_exit, true) - capture_log fn -> - assert %Mariaex.Error{} = query("DO SLEEP(0.1)", [], timeout: 0) - assert_receive {:EXIT, ^conn, {:shutdown, %DBConnection.ConnectionError{}}} - end + assert capture_log(fn -> + %DBConnection.ConnectionError{message: message} = query("DO SLEEP(10)", [], timeout: 50) + assert message =~ "tcp recv: closed" + assert_receive {:EXIT, ^conn, :killed}, 5000 + end) =~ "** (DBConnection.ConnectionError)" end test "support primitive data types using prepared statements", context do diff --git a/test/start_test.exs b/test/start_test.exs index 9b79cf5..9d26445 100644 --- a/test/start_test.exs +++ b/test/start_test.exs @@ -1,14 +1,25 @@ defmodule StartTest do use ExUnit.Case, async: true + import ExUnit.CaptureLog test "connection_errors" do - Process.flag :trap_exit, true - assert {:error, {%Mariaex.Error{mariadb: %{message: "Unknown database 'non_existing'"}}, _}} = - Mariaex.Connection.start_link(username: "mariaex_user", password: "mariaex_pass", database: "non_existing", sync_connect: true, backoff_type: :stop) - assert {:error, {%Mariaex.Error{mariadb: %{message: "Access denied for user " <> _}}, _}} = - Mariaex.Connection.start_link(username: "non_existing", database: "mariaex_test", sync_connect: true, backoff_type: :stop) - assert {:error, {%Mariaex.Error{message: "tcp connect: econnrefused"}, _}} = - Mariaex.Connection.start_link(username: "mariaex_user", password: "mariaex_pass", database: "mariaex_test", port: 60999, sync_connect: true, backoff_type: :stop) + Process.flag(:trap_exit, true) + opts = [sync_connect: true, backoff_type: :stop, max_restarts: 0] + + assert capture_log(fn -> + {:ok, pid} = Mariaex.start_link([username: "mariaex_user", password: "mariaex_pass", database: "non_existing"] ++ opts) + assert_receive {:EXIT, ^pid, :killed}, 5000 + end) =~ "** (Mariaex.Error) (1049): Unknown database 'non_existing'" + + assert capture_log(fn -> + {:ok, pid} = Mariaex.start_link([username: "non_existing", database: "mariaex_test"] ++ opts) + assert_receive {:EXIT, ^pid, :killed}, 5000 + end) =~ "** (Mariaex.Error) (1045): Access denied for user 'non_existing'" + + assert capture_log(fn -> + {:ok, pid} = Mariaex.start_link([username: "mariaex_user", password: "mariaex_pass", database: "mariaex_test", port: 60999] ++ opts) + assert_receive {:EXIT, ^pid, :killed}, 5000 + end) =~ "** (Mariaex.Error) tcp connect: econnrefused" end ## Tests tagged with :ssl_tests are excluded from running by default (see test_helper.exs) diff --git a/test/stream_test.exs b/test/stream_test.exs index 0cc25f7..1ca15da 100644 --- a/test/stream_test.exs +++ b/test/stream_test.exs @@ -17,9 +17,8 @@ defmodule StreamTest do test "simple text stream", context do assert Mariaex.transaction(context[:pid], fn(conn) -> - stream = Mariaex.stream(conn, "SELECT * FROM stream", [], []) - assert [%Mariaex.Result{num_rows: 0, rows: []}, - %Mariaex.Result{num_rows: 2, rows: [[1, "foo"], [2, "bar"]]}] = + stream = Mariaex.stream(conn, "SELECT * FROM stream", [], [query_type: :text]) + assert [%Mariaex.Result{num_rows: 2, rows: [[1, "foo"], [2, "bar"]]}] = Enum.to_list(stream) :done end) == {:ok, :done} diff --git a/test/test_helper.exs b/test/test_helper.exs index 0834724..934cbe2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -101,7 +101,7 @@ defmodule Mariaex.TestHelper do case Mariaex.Connection.query(pid, unquote(stat), unquote(params), unquote(opts)) do {:ok, %Mariaex.Result{rows: nil}} -> :ok {:ok, %Mariaex.Result{rows: rows}} -> rows - {:error, %Mariaex.Error{} = err} -> err + {:error, err} -> err end end end @@ -113,7 +113,7 @@ defmodule Mariaex.TestHelper do case Mariaex.query(var!(context)[:pid], unquote(stat), unquote(params), opts) do {:ok, %Mariaex.Result{rows: nil}} -> :ok {:ok, %Mariaex.Result{rows: rows}} -> rows - {:error, %Mariaex.Error{} = err} -> err + {:error, err} -> err end end end @@ -134,7 +134,7 @@ defmodule Mariaex.TestHelper do quote do case Mariaex.prepare(var!(context)[:pid], unquote(stat), unquote(opts)) do {:ok, %Mariaex.Query{} = query} -> query - {:error, %Mariaex.Error{} = err} -> err + {:error, err} -> err end end end @@ -142,9 +142,18 @@ defmodule Mariaex.TestHelper do defmacro execute(query, params, opts \\ []) do quote do case Mariaex.execute(var!(context)[:pid], unquote(query), unquote(params), unquote(opts)) do - {:ok, %Mariaex.Result{rows: nil}} -> :ok - {:ok, %Mariaex.Result{rows: rows}} -> rows - {:error, %Mariaex.Error{} = err} -> err + {:ok, %Mariaex.Query{}, %Mariaex.Result{rows: nil}} -> :ok + {:ok, %Mariaex.Query{}, %Mariaex.Result{rows: rows}} -> rows + {:error, err} -> err + end + end + end + + defmacro prepare_execute(name, statement, params, opts \\ []) do + quote do + case Mariaex.prepare_execute(var!(context)[:pid], unquote(name), unquote(statement), unquote(params), unquote(opts)) do + {:ok, %Mariaex.Query{} = query, %Mariaex.Result{rows: rows}} -> {query, rows} + {:error, err} -> err end end end @@ -153,17 +162,11 @@ defmodule Mariaex.TestHelper do quote do case Mariaex.close(var!(context)[:pid], unquote(query), unquote(opts)) do :ok -> :ok - {:error, %Mariaex.Error{} = err} -> err + {:error, err} -> err end end end - def capture_log(fun) do - Logger.remove_backend(:console) - fun.() - Logger.add_backend(:console, flush: true) - end - def length_encode_row(row) do Enum.map_join(row, &(<> <> &1)) end diff --git a/test/transaction_test.exs b/test/transaction_test.exs new file mode 100644 index 0000000..d8e01f0 --- /dev/null +++ b/test/transaction_test.exs @@ -0,0 +1,25 @@ +defmodule TransactionTest do + use ExUnit.Case + import Mariaex.TestHelper + + setup do + opts = [database: "mariaex_test", username: "mariaex_user", password: "mariaex_pass", backoff_type: :stop] + {:ok, pid} = Mariaex.Connection.start_link(opts) + {:ok, [pid: pid]} + end + + test "transaction shows correct transaction status", context do + pid = context[:pid] + opts = [mode: :transaction] + + assert DBConnection.status(pid, opts) == :idle + assert query("SELECT 42", []) == [[42]] + assert DBConnection.status(pid, opts) == :idle + DBConnection.transaction(pid, fn conn -> + assert DBConnection.status(conn, opts) == :transaction + end, opts) + assert DBConnection.status(pid, opts) == :idle + assert query("SELECT 42", []) == [[42]] + assert DBConnection.status(pid) == :idle + end +end