diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aa9c4b..940eb72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: ubuntu-16.04 + runs-on: ubuntu-18.04 strategy: fail-fast: false matrix: @@ -20,16 +20,18 @@ jobs: pair: - elixir: 1.11.3 otp: 23.2.5 + - elixir: 1.14.0 + otp: 25.1.2 include: - db: mysql:8.0 pair: - elixir: 1.11.3 - otp: 23.2.5 + elixir: 1.11.4 + otp: 23.3.3 lint: lint - db: mysql:8.0 pair: - elixir: 1.6.6 - otp: 19.3.6.13 + elixir: 1.7.4 + otp: 21.3.8.24 env: MIX_ENV: test DB: ${{matrix.db}} @@ -42,18 +44,24 @@ jobs: - uses: actions/checkout@v2 - - uses: erlef/setup-elixir@v1 + - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.pair.otp}} elixir-version: ${{matrix.pair.elixir}} - - name: Install Dependencies - run: mix deps.get --only test + - uses: actions/cache@v2 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} + + - run: mix deps.get - run: mix format --check-formatted if: ${{ matrix.lint }} - - run: mix deps.get && mix deps.unlock --check-unused + - run: mix deps.unlock --check-unused if: ${{ matrix.lint }} - run: mix deps.compile diff --git a/CHANGELOG.md b/CHANGELOG.md index 6674da2..bbaab3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## v0.6.3 (2022-09-22) + +* Print query statement in error log +* Add count to table reader metadata + +## v0.6.2 (2021-04-27) + +* Implement the Table.Reader protocol for query result + +## v0.6.1 (2022-01-25) + +* Revert allowing a given cache name to be reprepared as it leaks statements + +## v0.6.0 (2022-01-23) + +* Fix handling stored procedures with cursors +* Allow a given cache name to be reprepared +* Support queries returning multiple results +* Reuse prepared statements in `prepare: :unnamed` + +## v0.5.2 (2022-01-03) + +* Use optimized `Geo.WKB` API +* Update DBConnection +* Require Elixir v1.7 + ## v0.5.1 (2021-03-25) Bug fixes: diff --git a/README.md b/README.md index 5bd8ff8..90e53b2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Add `:myxql` to your dependencies: ```elixir def deps() do [ - {:myxql, "~> 0.5.0"} + {:myxql, "~> 0.6.0"} ] end ``` @@ -160,7 +160,7 @@ mix deps.get mix test ``` -See [`scripts/ci.sh`](scripts/ci.sh) and [`scripts/test-versions.sh`](scripts/test-versions.sh) for scripts used to test against different server versions. +See [`scripts/test-versions.sh`](scripts/test-versions.sh) for scripts used to test against different server versions. ## License diff --git a/lib/myxql.ex b/lib/myxql.ex index 84c67b7..07f7048 100644 --- a/lib/myxql.ex +++ b/lib/myxql.ex @@ -186,7 +186,7 @@ defmodule MyXQL do end @doc """ - Runs a query. + Runs a query that returns a single result. ## Text queries and prepared statements @@ -240,51 +240,125 @@ defmodule MyXQL do @spec query(conn, iodata, list, [query_option()]) :: {:ok, MyXQL.Result.t()} | {:error, Exception.t()} def query(conn, statement, params \\ [], options \\ []) when is_iodata(statement) do - if name = Keyword.get(options, :cache_statement) do - statement = IO.iodata_to_binary(statement) - query = %MyXQL.Query{name: name, statement: statement, cache: :statement} - - DBConnection.prepare_execute(conn, query, params, options) - |> query_result() - else - do_query(conn, statement, params, options) + name = options[:cache_statement] + query_type = options[:query_type] || :binary + + cond do + name != nil -> + statement = IO.iodata_to_binary(statement) + query = %MyXQL.Query{name: name, statement: statement, cache: :statement} + do_query(conn, query, params, options) + + query_type in [:binary, :binary_then_text] -> + query = %MyXQL.Query{name: "", statement: statement} + do_query(conn, query, params, options) + + query_type == :text -> + query = %MyXQL.TextQuery{statement: statement} + do_query(conn, query, params, options) end end - defp do_query(conn, statement, params, options) do - case Keyword.get(options, :query_type, :binary) do - :binary -> - prepare_execute(conn, "", statement, params, options) + @doc """ + Runs a query that returns a single result. - :binary_then_text -> - prepare_execute(conn, "", statement, params, options) + Returns `%MyXQL.Result{}` on success, or raises an exception if there was an error. - :text -> - DBConnection.execute(conn, %MyXQL.TextQuery{statement: statement}, params, options) + See `query/4`. + """ + @spec query!(conn, iodata, list, [query_option()]) :: MyXQL.Result.t() + def query!(conn, statement, params \\ [], opts \\ []) do + case query(conn, statement, params, opts) do + {:ok, result} -> result + {:error, exception} -> raise exception end - |> query_result() end - defp query_result({:ok, _query, result}), do: {:ok, result} - defp query_result({:error, _} = error), do: error + @doc """ + Runs a query that returns multiple results. + + A query may return multiple results if it is a text + query with statements separated by semicolons or a stored + procedure. Any prepared statement that is not a stored + procedure is not allowed to return multiple results and will + return an error. + + For more information on text queries and prepared statements, + see `query/4`. + + ## Options + + * `:query_type` - Use `:binary` for binary protocol (prepared statements), `:binary_then_text` to attempt + executing a binary query and if that fails fallback to executing a text query, and `:text` for text protocol + (default: `:binary`). + + * `:cache_statement` - Caches the query with the given name. If the cache statement + name is reused with a different statement, the previous query is automatically closed. + + Options are passed to `DBConnection.execute/4` for text protocol, and + `DBConnection.prepare_execute/4` for binary protocol. See their documentation for all available + options. + + ## Examples + + iex> MyXQL.query_many(conn, "SELECT 1; SELECT 2;", [], query_type: :text) + {:ok, [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}]} + + """ + @spec query_many(conn, iodata, list, [query_option()]) :: + {:ok, [MyXQL.Result.t()]} | {:error, Exception.t()} + def query_many(conn, statement, params \\ [], options \\ []) when is_iodata(statement) do + name = options[:cache_statement] + query_type = options[:query_type] || :binary + + cond do + name != nil -> + statement = IO.iodata_to_binary(statement) + query = %MyXQL.Queries{name: name, statement: statement, cache: :statement} + do_query(conn, query, params, options) + + query_type in [:binary, :binary_then_text] -> + query = %MyXQL.Queries{name: "", statement: statement} + do_query(conn, query, params, options) + + query_type == :text -> + query = %MyXQL.TextQueries{statement: statement} + do_query(conn, query, params, options) + end + end @doc """ - Runs a query. + Runs a query that returns multiple results. - Returns `%MyXQL.Result{}` on success, or raises an exception if there was an error. + Returns `[%MyXQL.Result{}]` on success, or raises an exception if there was an error. - See `query/4`. + See `query_many/4`. """ - @spec query!(conn, iodata, list, [query_option()]) :: MyXQL.Result.t() - def query!(conn, statement, params \\ [], opts \\ []) do - case query(conn, statement, params, opts) do + @spec query_many!(conn, iodata, list, [query_option()]) :: [MyXQL.Result.t()] + def query_many!(conn, statement, params \\ [], opts \\ []) do + case query_many(conn, statement, params, opts) do {:ok, result} -> result {:error, exception} -> raise exception end end + defp do_query(conn, %MyXQL.Query{} = query, params, options), + do: DBConnection.prepare_execute(conn, query, params, options) |> query_result() + + defp do_query(conn, %MyXQL.TextQuery{} = query, params, options), + do: DBConnection.execute(conn, query, params, options) |> query_result() + + defp do_query(conn, %MyXQL.Queries{} = query, params, options), + do: DBConnection.prepare_execute(conn, query, params, options) |> query_result() + + defp do_query(conn, %MyXQL.TextQueries{} = query, params, options), + do: DBConnection.execute(conn, query, params, options) |> query_result() + + defp query_result({:ok, _query, result}), do: {:ok, result} + defp query_result({:error, _} = error), do: error + @doc """ - Prepares a query to be later executed. + Prepares a query that returns a single result to be later executed. To execute the query, call `execute/4`. To close the query, call `close/3`. If a name is given, the name must be unique per query, as the name is cached @@ -312,7 +386,7 @@ defmodule MyXQL do end @doc """ - Prepares a query. + Prepares a query that returns a single result. Returns `%MyXQL.Query{}` on success, or raises an exception if there was an error. @@ -325,12 +399,55 @@ defmodule MyXQL do end @doc """ - Prepares and executes a query in a single step. + Prepares a query that returns multiple results to be later executed. - ## Multiple results + A prepared statement may return multiple results if it is a stored procedure. + Any other type of prepared statement is not allowed to return multiple results + and will return an error. - If a query returns multiple results (e.g. it's calling a procedure that returns multiple results) - an error is raised. If a query may return multiple results it's recommended to use `stream/4` instead. + To execute the query, call `execute_many/4`. To close the query, call `close/3`. + If a name is given, the name must be unique per query, as the name is cached. + If a new statement uses an old name, the old statement will be closed. + + ## Options + + Options are passed to `DBConnection.prepare/3`, see it's documentation for + all available options. + + ## Examples + + iex> {:ok, query} = MyXQL.prepare_many(conn, "", "CALL multi_procedure()") + iex> {:ok, [%MyXQL.Result{rows: [row1]}, %MyXQL.Result{rows: [row2]}]} = MyXQL.execute_many(conn, query, [2, 3]) + iex> row1 + [2] + iex> row2 + [3] + + """ + @spec prepare_many(conn(), iodata(), iodata(), [option()]) :: + {:ok, MyXQL.Queries.t()} | {:error, Exception.t()} + def prepare_many(conn, name, statement, opts \\ []) + when is_iodata(name) and is_iodata(statement) do + query = %MyXQL.Queries{name: name, statement: statement} + DBConnection.prepare(conn, query, opts) + end + + @doc """ + Prepares a query that returns multiple results. + + Returns `%MyXQL.Queries{}` on success, or raises an exception if there was an error. + + See `prepare_many/4`. + """ + @spec prepare_many!(conn(), iodata(), iodata(), [option()]) :: MyXQL.Queries.t() + def prepare_many!(conn, name, statement, opts \\ []) + when is_iodata(name) and is_iodata(statement) do + query = %MyXQL.Queries{name: name, statement: statement} + DBConnection.prepare!(conn, query, opts) + end + + @doc """ + Prepares and executes a query that returns a single result, in a single step. ## Options @@ -353,7 +470,7 @@ defmodule MyXQL do end @doc """ - Prepares and executes a query in a single step. + Prepares and executes a query that returns a single result, in a single step. Returns `{%MyXQL.Query{}, %MyXQL.Result{}}` on success, or raises an exception if there was an error. @@ -369,7 +486,52 @@ defmodule MyXQL do end @doc """ - Executes a prepared query. + Prepares and executes a query that returns multiple results, in a single step. + + A prepared statement may return multiple results if it is a stored procedure. + Any other type of prepared statement is not allowed to return multiple results + and will return an error. + + ## Options + + Options are passed to `DBConnection.prepare_execute/4`, see it's documentation for + all available options. + + ## Examples + + iex> {:ok, _, [%MyXQL.Result{rows: [row1]}, %MyXQL.Result{rows: [row2]}]} = MyXQL.prepare_execute(conn, "", "CALL multi_procedure()") + iex> row1 + [2] + iex> row2 + [3] + + """ + @spec prepare_execute_many(conn, iodata, iodata, list, keyword()) :: + {:ok, MyXQL.Queries.t(), [MyXQL.Result.t()]} | {:error, Exception.t()} + def prepare_execute_many(conn, name, statement, params \\ [], opts \\ []) + when is_iodata(name) and is_iodata(statement) do + query = %MyXQL.Queries{name: name, statement: statement} + DBConnection.prepare_execute(conn, query, params, opts) + end + + @doc """ + Prepares and executes a query that returns multiple results, in a single step. + + Returns `{%MyXQL.Queries{}, [%MyXQL.Result{}]}` on success, or raises an exception if there was + an error. + + See: `prepare_execute_many/5`. + """ + @spec prepare_execute_many!(conn, iodata, iodata, list, [option()]) :: + {MyXQL.Queries.t(), [MyXQL.Result.t()]} + def prepare_execute_many!(conn, name, statement, params \\ [], opts \\ []) + when is_iodata(name) and is_iodata(statement) do + query = %MyXQL.Queries{name: name, statement: statement} + DBConnection.prepare_execute!(conn, query, params, opts) + end + + @doc """ + Executes a prepared query that returns a single result. ## Options @@ -386,17 +548,57 @@ defmodule MyXQL do """ @spec execute(conn(), MyXQL.Query.t(), list(), [option()]) :: {:ok, MyXQL.Query.t(), MyXQL.Result.t()} | {:error, Exception.t()} - defdelegate execute(conn, query, params, opts \\ []), to: DBConnection + def execute(conn, %MyXQL.Query{} = query, params \\ [], opts \\ []) do + DBConnection.execute(conn, query, params, opts) + end @doc """ - Executes a prepared query. + Executes a prepared query that returns a single result. Returns `%MyXQL.Result{}` on success, or raises an exception if there was an error. See: `execute/4`. """ @spec execute!(conn(), MyXQL.Query.t(), list(), keyword()) :: MyXQL.Result.t() - defdelegate execute!(conn, query, params, opts \\ []), to: DBConnection + def execute!(conn, %MyXQL.Query{} = query, params \\ [], opts \\ []) do + DBConnection.execute!(conn, query, params, opts) + end + + @doc """ + Executes a prepared query that returns multiple results. + + ## Options + + Options are passed to `DBConnection.execute/4`, see it's documentation for + all available options. + + ## Examples + + iex> {:ok, query} = MyXQL.prepare_many(conn, "", "CALL multi_procedure()") + iex> {:ok, [%MyXQL.Result{rows: [row1]}, %MyXQL.Result{rows: [row2]}]} = MyXQL.execute_many(conn, query) + iex> row1 + [2] + iex> row2 + [3] + + """ + @spec execute_many(conn(), MyXQL.Queries.t(), list(), [option()]) :: + {:ok, MyXQL.Queries.t(), [MyXQL.Result.t()]} | {:error, Exception.t()} + def execute_many(conn, %MyXQL.Queries{} = query, params \\ [], opts \\ []) do + DBConnection.execute(conn, query, params, opts) + end + + @doc """ + Executes a prepared query that returns multiple results. + + Returns `[%MyXQL.Result{}]` on success, or raises an exception if there was an error. + + See: `execute_many/4`. + """ + @spec execute_many!(conn(), MyXQL.Queries.t(), list(), keyword()) :: [MyXQL.Result.t()] + def execute_many!(conn, %MyXQL.Queries{} = query, params \\ [], opts \\ []) do + DBConnection.execute!(conn, query, params, opts) + end @doc """ Closes a prepared query. @@ -408,8 +610,20 @@ defmodule MyXQL do Options are passed to `DBConnection.close/3`, see it's documentation for all available options. """ - @spec close(conn(), MyXQL.Query.t(), [option()]) :: :ok - def close(conn, %MyXQL.Query{} = query, opts \\ []) do + @spec close(conn(), MyXQL.Query.t() | MyXQL.Queries.t(), [option()]) :: :ok + def close(conn, query, opts \\ []) + + def close(conn, %MyXQL.Query{} = query, opts) do + case DBConnection.close(conn, query, opts) do + {:ok, _} -> + :ok + + {:error, _} = error -> + error + end + end + + def close(conn, %MyXQL.Queries{} = query, opts) do case DBConnection.close(conn, query, opts) do {:ok, _} -> :ok diff --git a/lib/myxql/client.ex b/lib/myxql/client.ex index 07ccf6c..e76c13c 100644 --- a/lib/myxql/client.ex +++ b/lib/myxql/client.ex @@ -102,27 +102,27 @@ defmodule MyXQL.Client do end end - def com_query(client, statement) do + def com_query(client, statement, result_state \\ :single) do with :ok <- send_com(client, {:com_query, statement}) do - recv_packets(client, &decode_com_query_response/3, :initial) + recv_packets(client, &decode_com_query_response/3, :initial, result_state) end end def com_stmt_prepare(client, statement) do with :ok <- send_com(client, {:com_stmt_prepare, statement}) do - recv_packets(client, &decode_com_stmt_prepare_response/3, :initial) + recv_packets(client, &decode_com_stmt_prepare_response/3, :initial, :single) end end - def com_stmt_execute(client, statement_id, params, cursor_type) do + def com_stmt_execute(client, statement_id, params, cursor_type, result_state \\ :single) do with :ok <- send_com(client, {:com_stmt_execute, statement_id, params, cursor_type}) do - recv_packets(client, &decode_com_stmt_execute_response/3, :initial) + recv_packets(client, &decode_com_stmt_execute_response/3, :initial, result_state) end end def com_stmt_fetch(client, statement_id, column_defs, max_rows) do with :ok <- send_com(client, {:com_stmt_fetch, statement_id, max_rows}) do - recv_packets(client, &decode_com_stmt_fetch_response/3, {:initial, column_defs}) + recv_packets(client, &decode_com_stmt_fetch_response/3, {:initial, column_defs}, :single) end end @@ -171,13 +171,13 @@ defmodule MyXQL.Client do def recv_packet(client, decoder, timeout \\ :infinity) do # even if next packet follows, ignore it new_decoder = fn payload, _next_packet, nil -> {:halt, decoder.(payload)} end - recv_packets(client, new_decoder, nil, timeout) + recv_packets(client, new_decoder, nil, :single, timeout) end - def recv_packets(client, decoder, decoder_state, timeout \\ :infinity) do + def recv_packets(client, decoder, decoder_state, result_state, timeout \\ :infinity) do case recv_data(client, timeout) do {:ok, data} -> - recv_packets(data, decoder, decoder_state, timeout, client) + recv_packets(data, decoder, decoder_state, result_state, timeout, client) {:error, _} = error -> error @@ -190,12 +190,35 @@ defmodule MyXQL.Client do ## Internals - defp recv_packets(data, decode, decoder_state, timeout, client, partial \\ <<>>) + defp recv_packets(data, decode, decoder_state, result_state, timeout, client, partial \\ <<>>) defp recv_packets( - <>, + <> = data, + decoder, + {:more_results, resultset}, + result_state, + timeout, + client, + partial + ) + when size < @default_max_packet_size do + case decode_more_results(<>, rest, resultset, result_state) do + {:cont, decoder_state, result_state} -> + recv_packets(data, decoder, decoder_state, result_state, timeout, client, partial) + + {:halt, result} -> + {:ok, result} + + {:error, _} = error -> + error + end + end + + defp recv_packets( + <>, decoder, decoder_state, + result_state, timeout, client, partial @@ -203,10 +226,13 @@ defmodule MyXQL.Client do when size < @default_max_packet_size do case decoder.(<>, rest, decoder_state) do {:cont, decoder_state} -> - recv_packets(rest, decoder, decoder_state, timeout, client) + recv_packets(rest, decoder, decoder_state, result_state, timeout, client) {:halt, result} -> - {:ok, result} + case result_state do + :single -> {:ok, result} + {:many, results} -> {:ok, [result | results]} + end {:error, _} = error -> error @@ -216,9 +242,10 @@ defmodule MyXQL.Client do # If the packet size equals max packet size, save the payload, receive # more data and try again defp recv_packets( - <>, + <>, decoder, decoder_state, + result_state, timeout, client, partial @@ -228,6 +255,7 @@ defmodule MyXQL.Client do rest, decoder, decoder_state, + result_state, timeout, client, <> @@ -235,13 +263,14 @@ defmodule MyXQL.Client do end # If we didn't match on a full packet, receive more data and try again - defp recv_packets(rest, decoder, decoder_state, timeout, client, partial) do + defp recv_packets(rest, decoder, decoder_state, result_state, timeout, client, partial) do case recv_data(client, timeout) do {:ok, data} -> recv_packets( <>, decoder, decoder_state, + result_state, timeout, client, partial diff --git a/lib/myxql/connection.ex b/lib/myxql/connection.ex index 06ed9ca..408cfa3 100644 --- a/lib/myxql/connection.ex +++ b/lib/myxql/connection.ex @@ -3,7 +3,7 @@ defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol.{Flags, Records} - alias MyXQL.{Client, Cursor, Query, Protocol, Result, TextQuery} + alias MyXQL.{Client, Cursor, Query, Queries, Protocol, Result, TextQuery, TextQueries} defstruct [ :client, @@ -59,11 +59,6 @@ defmodule MyXQL.Connection do {:ok, state} end - @impl true - def checkin(state) do - {:ok, state} - end - @impl true def handle_prepare(query, opts, state) do query = rename_query(state, query) @@ -90,14 +85,25 @@ defmodule MyXQL.Connection do end @impl true - def handle_execute(%Query{} = query, params, _opts, state) do + def handle_execute(%TextQuery{statement: statement} = query, [], _opts, state) do + Client.com_query(state.client, statement, result_state(query)) + |> result(query, state) + end + + def handle_execute(%TextQueries{statement: statement} = query, [], _opts, state) do + Client.com_query(state.client, statement, result_state(query)) + |> result(query, state) + end + + def handle_execute(query, params, _opts, state) do with {:ok, query, state} <- maybe_reprepare(query, state) do result = Client.com_stmt_execute( state.client, query.statement_id, params, - :cursor_type_no_cursor + :cursor_type_no_cursor, + result_state(query) ) with {:ok, state} <- maybe_close(query, state) do @@ -106,13 +112,8 @@ defmodule MyXQL.Connection do end end - def handle_execute(%TextQuery{statement: statement} = query, [], _opts, state) do - Client.com_query(state.client, statement) - |> result(query, state) - end - @impl true - def handle_close(%Query{} = query, _opts, state) do + def handle_close(query, _opts, state) do with {:ok, state} <- close(query, state) do {:ok, nil, state} end @@ -317,13 +318,47 @@ defmodule MyXQL.Connection do {:ok, query, result, put_status(state, status_flags)} end + defp result({:ok, resultsets}, query, state) when is_list(resultsets) do + {results, status_flags} = + Enum.reduce(resultsets, {[], nil}, fn resultset, {results, newest_status_flags} -> + resultset( + column_defs: column_defs, + num_rows: num_rows, + rows: rows, + status_flags: status_flags, + num_warnings: num_warnings + ) = resultset + + columns = Enum.map(column_defs, &elem(&1, 1)) + + result = %Result{ + connection_id: state.client.connection_id, + columns: columns, + num_rows: num_rows, + rows: rows, + num_warnings: num_warnings + } + + # Keep status flags from the last query. The resultsets + # are given to this function in reverse order, so it is the first one. + if newest_status_flags do + {[result | results], newest_status_flags} + else + {[result | results], status_flags} + end + end) + + {:ok, query, results, put_status(state, status_flags)} + end + defp result({:ok, err_packet() = err_packet}, query, state) do exception = error(err_packet, query, state) maybe_disconnect(exception, state) end defp result({:error, :multiple_results}, _query, _state) do - raise RuntimeError, "returning multiple results is not yet supported" + raise RuntimeError, + "returning multiple results is not supported from this function. Use MyXQL.query_many/4 and similar functions." end defp result({:error, reason}, _query, state) do @@ -403,7 +438,12 @@ defmodule MyXQL.Connection do {:ok, result, state} other -> - result(other, statement, state) + # We convert {:error, exception, state} to {:error, state} + # so that DBConnection will disconnect during handle_begin/handle_rollback + # and will attempt to rollback during handle_commit + with {:error, _exception, state} <- result(other, statement, state) do + {:error, state} + end end end @@ -428,8 +468,8 @@ defmodule MyXQL.Connection do defp rename_query(%{prepare: :unnamed}, query), do: %{query | name: ""} - defp prepare(%Query{statement: statement} = query, state) do - case Client.com_stmt_prepare(state.client, statement) do + defp prepare(query, state) do + case Client.com_stmt_prepare(state.client, query.statement) do {:ok, com_stmt_prepare_ok(statement_id: statement_id, num_params: num_params)} -> ref = make_ref() query = %{query | num_params: num_params, statement_id: statement_id, ref: ref} @@ -456,7 +496,7 @@ defmodule MyXQL.Connection do end # Close unnamed queries after executing them - defp maybe_close(%Query{name: ""} = query, state), do: close(query, state) + defp maybe_close(%{name: ""} = query, state), do: close(query, state) defp maybe_close(_query, state), do: {:ok, state} defp close(%{ref: ref} = query, %{last_ref: ref} = state) do @@ -474,14 +514,19 @@ defmodule MyXQL.Connection do end end + defp result_state(%TextQuery{}), do: :single + defp result_state(%TextQueries{}), do: {:many, []} + defp result_state(%Query{}), do: :single + defp result_state(%Queries{}), do: {:many, []} + ## Cache query handling defp queries_new(), do: :ets.new(__MODULE__, [:set, :public]) defp queries_put(%{queries: nil}, _), do: :ok - defp queries_put(_state, %Query{name: ""}), do: :ok + defp queries_put(_state, %{name: ""}), do: :ok - defp queries_put(state, %Query{cache: :reference} = query) do + defp queries_put(state, %{cache: :reference} = query) do %{ num_params: num_params, statement_id: statement_id, @@ -499,7 +544,7 @@ defmodule MyXQL.Connection do end end - defp queries_put(state, %Query{cache: :statement} = query) do + defp queries_put(state, %{cache: :statement} = query) do %{ num_params: num_params, statement_id: statement_id, @@ -519,9 +564,9 @@ defmodule MyXQL.Connection do end defp queries_delete(%{queries: nil}, _), do: :ok - defp queries_delete(_state, %Query{name: ""}), do: :ok + defp queries_delete(_state, %{name: ""}), do: :ok - defp queries_delete(state, %Query{name: name}) do + defp queries_delete(state, %{name: name}) do try do :ets.delete(state.queries, name) rescue @@ -532,9 +577,9 @@ defmodule MyXQL.Connection do end defp queries_get(%{queries: nil}, _), do: nil - defp queries_get(_state, %Query{name: ""}), do: nil + defp queries_get(_state, %{name: ""}), do: nil - defp queries_get(state, %Query{cache: :reference, name: name} = query) do + defp queries_get(state, %{cache: :reference, name: name} = query) do try do :ets.lookup_element(state.queries, name, 2) rescue @@ -545,7 +590,7 @@ defmodule MyXQL.Connection do end end - defp queries_get(state, %Query{cache: :statement, name: name, statement: statement} = query) do + defp queries_get(state, %{cache: :statement, name: name, statement: statement} = query) do try do :ets.lookup_element(state.queries, name, 2) rescue diff --git a/lib/myxql/error.ex b/lib/myxql/error.ex index 5954350..ce21950 100644 --- a/lib/myxql/error.ex +++ b/lib/myxql/error.ex @@ -14,11 +14,22 @@ defmodule MyXQL.Error do } @impl true - def message(%{mysql: %{code: code, name: nil}, message: message}) do - "(#{code}) " <> message + def message(e) do + if map = e.mysql do + IO.iodata_to_binary([ + [?(, Integer.to_string(map.code), ?)], + build_name(map), + e.message, + build_query(e.statement) + ]) + else + e.message + end end - def message(%{mysql: %{code: code, name: name}, message: message}) do - "(#{code}) (#{name}) " <> message - end + defp build_name(%{name: nil}), do: [?\s] + defp build_name(%{name: name}), do: [?\s, ?(, Atom.to_string(name), ?), ?\s] + + defp build_query(nil), do: [] + defp build_query(query_statement), do: ["\n\n query: ", query_statement] end diff --git a/lib/myxql/protocol.ex b/lib/myxql/protocol.ex index 1de5aca..f74f95b 100644 --- a/lib/myxql/protocol.ex +++ b/lib/myxql/protocol.ex @@ -3,7 +3,7 @@ defmodule MyXQL.Protocol do import MyXQL.Protocol.{Flags, Records, Types} alias MyXQL.Protocol.Values - use Bitwise + import Bitwise defdelegate error_code_to_name(code), to: MyXQL.Protocol.ServerErrorCodes, as: :code_to_name @@ -37,7 +37,7 @@ defmodule MyXQL.Protocol do encode_packet(rest, rest_size, next_sequence_id, max_packet_size) ] else - [<>, payload] + [<>, payload] end end @@ -54,8 +54,8 @@ defmodule MyXQL.Protocol do {last_insert_id, rest} = take_int_lenenc(rest) << - status_flags::uint2, - num_warnings::uint2, + status_flags::uint2(), + num_warnings::uint2(), info::binary >> = rest @@ -69,7 +69,7 @@ defmodule MyXQL.Protocol do end defp decode_err_packet_body( - <> + <> ) do err_packet(code: code, message: message) end @@ -78,14 +78,14 @@ defmodule MyXQL.Protocol do decode_eof_packet_body(rest) end - defp decode_eof_packet_body(<>) do + defp decode_eof_packet_body(<>) do eof_packet( status_flags: status_flags, num_warnings: num_warnings ) end - defp decode_connect_err_packet_body(<>) do + defp decode_connect_err_packet_body(<>) do err_packet(code: code, message: message) end @@ -99,23 +99,23 @@ defmodule MyXQL.Protocol do {server_version, rest} = take_string_nul(rest) << - conn_id::uint4, + conn_id::uint4(), auth_plugin_data1::string(8), 0, - capability_flags1::uint2, - charset::uint1, - status_flags::uint2, - capability_flags2::uint2, + capability_flags1::uint2(), + charset::uint1(), + status_flags::uint2(), + capability_flags2::uint2(), rest::binary >> = rest - <> = <> + <> = <> # all set in servers since MySQL 4.1 required_capabilities = [:client_protocol_41, :client_secure_connection] with :ok <- ensure_capabilities(capability_flags, required_capabilities) do << - auth_plugin_data_length::uint1, + auth_plugin_data_length::uint1(), _::uint(10), rest::binary >> = rest @@ -159,6 +159,7 @@ defmodule MyXQL.Protocol do put_capability_flags([ :client_protocol_41, :client_secure_connection, + :client_multi_statements, # set by servers since 4.0 :client_transactions ]) @@ -194,8 +195,8 @@ defmodule MyXQL.Protocol do database = if database, do: <>, else: "" << - capability_flags::uint4, - max_packet_size::uint4, + capability_flags::uint4(), + max_packet_size::uint4(), charset, 0::uint(23), <>, @@ -213,8 +214,8 @@ defmodule MyXQL.Protocol do ) ) do << - capability_flags::uint4, - max_packet_size::uint4, + capability_flags::uint4(), + max_packet_size::uint4(), charset, 0::uint(23) >> @@ -272,12 +273,12 @@ defmodule MyXQL.Protocol do # https://dev.mysql.com/doc/internals/en/com-stmt-close.html def encode_com({:com_stmt_close, statement_id}) do - [0x19, <>] + [0x19, <>] end # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html def encode_com({:com_stmt_reset, statement_id}) do - [0x1A, <>] + [0x1A, <>] end # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html @@ -295,9 +296,9 @@ defmodule MyXQL.Protocol do << command, - statement_id::uint4, - flags::uint1, - iteration_count::uint4, + statement_id::uint4(), + flags::uint1(), + iteration_count::uint4(), params::binary >> end @@ -306,8 +307,8 @@ defmodule MyXQL.Protocol do def encode_com({:com_stmt_fetch, statement_id, num_rows}) do << 0x1C, - statement_id::uint4, - num_rows::uint4 + statement_id::uint4(), + num_rows::uint4() >> end @@ -325,8 +326,8 @@ defmodule MyXQL.Protocol do end def decode_com_stmt_prepare_response( - <<0x00, statement_id::uint4, num_columns::uint2, num_params::uint2, 0, - num_warnings::uint2>>, + <<0x00, statement_id::uint4(), num_columns::uint2(), num_params::uint2(), 0, + num_warnings::uint2()>>, next_data, :initial ) do @@ -419,7 +420,7 @@ defmodule MyXQL.Protocol do null_bitmap_size = div(count + 7, 8) new_params_bound_flag = 1 - <> end @@ -461,12 +462,12 @@ defmodule MyXQL.Protocol do << 0x0C, - _character_set::uint2, - column_length::uint4, - type::uint1, - flags::uint2, - _decimals::uint1, - 0::uint2 + _character_set::uint2(), + column_length::uint4(), + type::uint1(), + flags::uint2(), + _decimals::uint1(), + 0::uint2() >> = rest column_def( @@ -478,6 +479,26 @@ defmodule MyXQL.Protocol do ) end + def decode_more_results(payload, "", resultset, result_state) do + ok_packet(status_flags: status_flags) = decode_generic_response(payload) + + case result_state do + :single -> + {:halt, resultset(resultset, status_flags: status_flags)} + + {:many, results} -> + {:halt, [resultset(resultset, status_flags: status_flags) | results]} + end + end + + def decode_more_results(_payload, _next_data, _resultset, :single) do + {:error, :multiple_results} + end + + def decode_more_results(_payload, _next_data, resultset, {:many, results}) do + {:cont, :initial, {:many, [resultset | results]}} + end + defp decode_resultset(payload, _next_data, :initial, _row_decoder) do {:cont, {:column_defs, decode_int_lenenc(payload), []}} end @@ -494,12 +515,13 @@ defmodule MyXQL.Protocol do end defp decode_resultset( - <<0xFE, num_warnings::uint2, status_flags::uint2>>, + <<0xFE, num_warnings::uint2(), status_flags::uint2()>>, next_data, {:column_defs_eof, column_defs}, _row_decoder ) do - if has_status_flag?(status_flags, :server_status_cursor_exists) do + if has_status_flag?(status_flags, :server_status_cursor_exists) and + not has_status_flag?(status_flags, :server_more_results_exists) do "" = next_data {:halt, @@ -516,7 +538,7 @@ defmodule MyXQL.Protocol do end defp decode_resultset( - <<0xFE, num_warnings::uint2, status_flags::uint2>>, + <<0xFE, num_warnings::uint2(), status_flags::uint2()>>, _next_data, {:rows, column_defs, num_rows, acc}, _row_decoder @@ -531,7 +553,7 @@ defmodule MyXQL.Protocol do ) if has_status_flag?(status_flags, :server_more_results_exists) do - {:cont, {:trailing_ok_packet, resultset}} + {:cont, {:more_results, resultset}} else {:halt, resultset} end @@ -545,13 +567,4 @@ defmodule MyXQL.Protocol do row = row_decoder.(payload, column_defs) {:cont, {:rows, column_defs, num_rows + 1, [row | acc]}} end - - defp decode_resultset(payload, "", {:trailing_ok_packet, resultset}, _row_decoder) do - ok_packet(status_flags: status_flags) = decode_generic_response(payload) - {:halt, resultset(resultset, status_flags: status_flags)} - end - - defp decode_resultset(_payload, _next_data, {:trailing_ok_packet, _resultset}, _row_decoder) do - {:error, :multiple_results} - end end diff --git a/lib/myxql/protocol/flags.ex b/lib/myxql/protocol/flags.ex index dfd9150..ada7d44 100644 --- a/lib/myxql/protocol/flags.ex +++ b/lib/myxql/protocol/flags.ex @@ -1,7 +1,7 @@ defmodule MyXQL.Protocol.Flags do @moduledoc false - use Bitwise + import Bitwise # https://dev.mysql.com/doc/internals/en/capability-flags.html @capability_flags [ diff --git a/lib/myxql/protocol/server_error_codes.ex b/lib/myxql/protocol/server_error_codes.ex index 1107111..35e1c27 100644 --- a/lib/myxql/protocol/server_error_codes.ex +++ b/lib/myxql/protocol/server_error_codes.ex @@ -1,7 +1,15 @@ defmodule MyXQL.Protocol.ServerErrorCodes do @moduledoc false - codes = [ + # TODO: remove when we require Elixir v1.10+ + codes_from_config = + if Version.match?(System.version(), ">= 1.10.0") do + Application.compile_env(:myxql, :extra_error_codes, []) + else + apply(Application, :get_env, [:myxql, :extra_error_codes, []]) + end + + default_codes = [ {1005, :ER_CANT_CREATE_TABLE}, {1006, :ER_CANT_CREATE_DB}, {1007, :ER_DB_CREATE_EXISTS}, @@ -23,8 +31,7 @@ defmodule MyXQL.Protocol.ServerErrorCodes do {1836, :ER_READ_ONLY_MODE} ] - # TODO: use Application.compile_env/3 when we require Elixir v1.10 - codes = codes ++ Application.get_env(:myxql, :extra_error_codes, []) + codes = default_codes ++ codes_from_config for {code, name} <- Enum.uniq(codes) do def name_to_code(unquote(name)), do: unquote(code) diff --git a/lib/myxql/protocol/types.ex b/lib/myxql/protocol/types.ex index ab0db68..a1ab7d4 100644 --- a/lib/myxql/protocol/types.ex +++ b/lib/myxql/protocol/types.ex @@ -22,19 +22,19 @@ defmodule MyXQL.Protocol.Types do # https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::LengthEncodedInteger def encode_int_lenenc(int) when int < 251, do: <> - def encode_int_lenenc(int) when int < 0xFFFF, do: <<0xFC, int::uint2>> - def encode_int_lenenc(int) when int < 0xFFFFFF, do: <<0xFD, int::uint3>> - def encode_int_lenenc(int) when int < 0xFFFFFFFFFFFFFFFF, do: <<0xFE, int::uint8>> + def encode_int_lenenc(int) when int < 0xFFFF, do: <<0xFC, int::uint2()>> + def encode_int_lenenc(int) when int < 0xFFFFFF, do: <<0xFD, int::uint3()>> + def encode_int_lenenc(int) when int < 0xFFFFFFFFFFFFFFFF, do: <<0xFE, int::uint8()>> def decode_int_lenenc(binary) do {integer, ""} = take_int_lenenc(binary) integer end - def take_int_lenenc(<>) when int < 251, do: {int, rest} - def take_int_lenenc(<<0xFC, int::uint2, rest::binary>>), do: {int, rest} - def take_int_lenenc(<<0xFD, int::uint3, rest::binary>>), do: {int, rest} - def take_int_lenenc(<<0xFE, int::uint8, rest::binary>>), do: {int, rest} + def take_int_lenenc(<>) when int < 251, do: {int, rest} + def take_int_lenenc(<<0xFC, int::uint2(), rest::binary>>), do: {int, rest} + def take_int_lenenc(<<0xFD, int::uint3(), rest::binary>>), do: {int, rest} + def take_int_lenenc(<<0xFE, int::uint8(), rest::binary>>), do: {int, rest} # https://dev.mysql.com/doc/internals/en/string.html#packet-Protocol::FixedLengthString defmacro string(size) do diff --git a/lib/myxql/protocol/values.ex b/lib/myxql/protocol/values.ex index ece9c03..4882696 100644 --- a/lib/myxql/protocol/values.ex +++ b/lib/myxql/protocol/values.ex @@ -1,7 +1,7 @@ defmodule MyXQL.Protocol.Values do @moduledoc false - use Bitwise + import Bitwise import MyXQL.Protocol.Types import MyXQL.Protocol.Records, only: [column_def: 1] @@ -187,7 +187,7 @@ defmodule MyXQL.Protocol.Values do def encode_binary_value(value) when is_integer(value) and value >= -1 <<< 63 and value < 1 <<< 64 do - {:mysql_type_longlong, <>} + {:mysql_type_longlong, <>} end def encode_binary_value(value) when is_float(value) do @@ -202,11 +202,11 @@ defmodule MyXQL.Protocol.Values do end def encode_binary_value(%Date{year: year, month: month, day: day}) do - {:mysql_type_date, <<4, year::uint2, month::uint1, day::uint1>>} + {:mysql_type_date, <<4, year::uint2(), month::uint1(), day::uint1()>>} end def encode_binary_value(:zero_date) do - {:mysql_type_date, <<4, 0::uint2, 0::uint1, 0::uint1>>} + {:mysql_type_date, <<4, 0::uint2(), 0::uint1(), 0::uint1()>>} end def encode_binary_value(%Time{} = time), do: encode_binary_time(time) @@ -269,7 +269,7 @@ defmodule MyXQL.Protocol.Values do defp encode_geometry(geo) do srid = geo.srid || 0 binary = %{geo | srid: nil} |> Geo.WKB.encode_to_iodata(:ndr) |> IO.iodata_to_binary() - {:mysql_type_var_string, encode_string_lenenc(<>)} + {:mysql_type_var_string, encode_string_lenenc(<>)} end end @@ -283,7 +283,8 @@ defmodule MyXQL.Protocol.Values do end defp encode_binary_time(%Time{hour: hour, minute: minute, second: second, microsecond: {0, 0}}) do - {:mysql_type_time, <<8, 0::uint1, 0::uint4, hour::uint1, minute::uint1, second::uint1>>} + {:mysql_type_time, + <<8, 0::uint1(), 0::uint4(), hour::uint1(), minute::uint1(), second::uint1()>>} end defp encode_binary_time(%Time{ @@ -293,7 +294,8 @@ defmodule MyXQL.Protocol.Values do microsecond: {microsecond, _} }) do {:mysql_type_time, - <<12, 0::uint1, 0::uint4, hour::uint1, minute::uint1, second::uint1, microsecond::uint4>>} + <<12, 0::uint1(), 0::uint4(), hour::uint1(), minute::uint1(), second::uint1(), + microsecond::uint4()>>} end defp encode_binary_datetime(%NaiveDateTime{ @@ -306,7 +308,8 @@ defmodule MyXQL.Protocol.Values do microsecond: {0, 0} }) do {:mysql_type_datetime, - <<7, year::uint2, month::uint1, day::uint1, hour::uint1, minute::uint1, second::uint1>>} + <<7, year::uint2(), month::uint1(), day::uint1(), hour::uint1(), minute::uint1(), + second::uint1()>>} end defp encode_binary_datetime(%NaiveDateTime{ @@ -319,8 +322,8 @@ defmodule MyXQL.Protocol.Values do microsecond: {microsecond, _} }) do {:mysql_type_datetime, - <<11, year::uint2, month::uint1, day::uint1, hour::uint1, minute::uint1, second::uint1, - microsecond::uint4>>} + <<11, year::uint2(), month::uint1(), day::uint1(), hour::uint1(), minute::uint1(), + second::uint1(), microsecond::uint4()>>} end defp encode_binary_datetime(%DateTime{ @@ -334,8 +337,8 @@ defmodule MyXQL.Protocol.Values do time_zone: "Etc/UTC" }) do {:mysql_type_datetime, - <<11, year::uint2, month::uint1, day::uint1, hour::uint1, minute::uint1, second::uint1, - microsecond::uint4>>} + <<11, year::uint2(), month::uint1(), day::uint1(), hour::uint1(), minute::uint1(), + second::uint1(), microsecond::uint4()>>} end defp encode_binary_datetime(%DateTime{} = datetime) do @@ -418,7 +421,7 @@ defmodule MyXQL.Protocol.Values do if Code.ensure_loaded?(Geo) do # https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-internal-format - defp decode_geometry(<>) do + defp decode_geometry(<>) do srid = if srid == 0, do: nil, else: srid r |> Geo.WKB.decode!() |> Map.put(:srid, srid) end @@ -434,28 +437,28 @@ defmodule MyXQL.Protocol.Values do end end - defp decode_int1(<>, null_bitmap, t, acc), + defp decode_int1(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_uint1(<>, null_bitmap, t, acc), + defp decode_uint1(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_int2(<>, null_bitmap, t, acc), + defp decode_int2(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_uint2(<>, null_bitmap, t, acc), + defp decode_uint2(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_int4(<>, null_bitmap, t, acc), + defp decode_int4(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_uint4(<>, null_bitmap, t, acc), + defp decode_uint4(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_int8(<>, null_bitmap, t, acc), + defp decode_int8(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) - defp decode_uint8(<>, null_bitmap, t, acc), + defp decode_uint8(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) defp decode_float(<>, null_bitmap, t, acc), @@ -466,10 +469,15 @@ defmodule MyXQL.Protocol.Values do # in theory it's supposed to be a `string_lenenc` field. However since MySQL decimals # maximum precision is 65 digits, the size of the string will always fir on one byte. - defp decode_decimal(<>, null_bitmap, t, acc), + defp decode_decimal(<>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [Decimal.new(string) | acc]) - defp decode_date(<<4, year::uint2, month::uint1, day::uint1, r::bits>>, null_bitmap, t, acc) do + defp decode_date( + <<4, year::uint2(), month::uint1(), day::uint1(), r::bits>>, + null_bitmap, + t, + acc + ) do v = %Date{year: year, month: month, day: day} decode_binary_row(r, null_bitmap >>> 1, t, [v | acc]) end @@ -480,7 +488,8 @@ defmodule MyXQL.Protocol.Values do end defp decode_time( - <<8, is_negative, days::uint4, hours::uint1, minutes::uint1, seconds::uint1, r::bits>>, + <<8, is_negative, days::uint4(), hours::uint1(), minutes::uint1(), seconds::uint1(), + r::bits>>, null_bitmap, t, acc @@ -490,8 +499,8 @@ defmodule MyXQL.Protocol.Values do end defp decode_time( - <<12, is_negative, days::uint4, hours::uint1, minutes::uint1, seconds::uint1, - microseconds::uint4, r::bits>>, + <<12, is_negative, days::uint4(), hours::uint1(), minutes::uint1(), seconds::uint1(), + microseconds::uint4(), r::bits>>, null_bitmap, t, acc @@ -521,7 +530,7 @@ defmodule MyXQL.Protocol.Values do end defp decode_datetime( - <<4, year::uint2, month::uint1, day::uint1, r::bits>>, + <<4, year::uint2(), month::uint1(), day::uint1(), r::bits>>, null_bitmap, t, acc, @@ -532,8 +541,8 @@ defmodule MyXQL.Protocol.Values do end defp decode_datetime( - <<7, year::uint2, month::uint1, day::uint1, hour::uint1, minute::uint1, second::uint1, - r::bits>>, + <<7, year::uint2(), month::uint1(), day::uint1(), hour::uint1(), minute::uint1(), + second::uint1(), r::bits>>, null_bitmap, t, acc, @@ -544,8 +553,8 @@ defmodule MyXQL.Protocol.Values do end defp decode_datetime( - <<11, year::uint2, month::uint1, day::uint1, hour::uint1, minute::uint1, second::uint1, - microsecond::uint4, r::bits>>, + <<11, year::uint2(), month::uint1(), day::uint1(), hour::uint1(), minute::uint1(), + second::uint1(), microsecond::uint4(), r::bits>>, null_bitmap, t, acc, @@ -594,12 +603,12 @@ defmodule MyXQL.Protocol.Values do } end - defp decode_string_lenenc(<>, null_bitmap, t, acc, decoder) + defp decode_string_lenenc(<>, null_bitmap, t, acc, decoder) when n < 251, do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc]) defp decode_string_lenenc( - <<0xFC, n::uint2, v::string(n), r::bits>>, + <<0xFC, n::uint2(), v::string(n), r::bits>>, null_bitmap, t, acc, @@ -608,7 +617,7 @@ defmodule MyXQL.Protocol.Values do do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc]) defp decode_string_lenenc( - <<0xFD, n::uint3, v::string(n), r::bits>>, + <<0xFD, n::uint3(), v::string(n), r::bits>>, null_bitmap, t, acc, @@ -617,7 +626,7 @@ defmodule MyXQL.Protocol.Values do do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc]) defp decode_string_lenenc( - <<0xFE, n::uint8, v::string(n), r::bits>>, + <<0xFE, n::uint8(), v::string(n), r::bits>>, null_bitmap, t, acc, @@ -625,16 +634,16 @@ defmodule MyXQL.Protocol.Values do ), do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc]) - defp decode_json(<>, null_bitmap, t, acc) when n < 251, + defp decode_json(<>, null_bitmap, t, acc) when n < 251, do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_json(v) | acc]) - defp decode_json(<<0xFC, n::uint2, v::string(n), r::bits>>, null_bitmap, t, acc), + defp decode_json(<<0xFC, n::uint2(), v::string(n), r::bits>>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_json(v) | acc]) - defp decode_json(<<0xFD, n::uint3, v::string(n), r::bits>>, null_bitmap, t, acc), + defp decode_json(<<0xFD, n::uint3(), v::string(n), r::bits>>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_json(v) | acc]) - defp decode_json(<<0xFE, n::uint8, v::string(n), r::bits>>, null_bitmap, t, acc), + defp decode_json(<<0xFE, n::uint8(), v::string(n), r::bits>>, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_json(v) | acc]) defp decode_json(string), do: json_library().decode!(string) @@ -643,16 +652,16 @@ defmodule MyXQL.Protocol.Values do Application.get_env(:myxql, :json_library, Jason) end - defp decode_bit(<>, size, null_bitmap, t, acc) when n < 251, + defp decode_bit(<>, size, null_bitmap, t, acc) when n < 251, do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_bit(v, size) | acc]) - defp decode_bit(<<0xFC, n::uint2, v::string(n), r::bits>>, size, null_bitmap, t, acc), + defp decode_bit(<<0xFC, n::uint2(), v::string(n), r::bits>>, size, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_bit(v, size) | acc]) - defp decode_bit(<<0xFD, n::uint3, v::string(n), r::bits>>, size, null_bitmap, t, acc), + defp decode_bit(<<0xFD, n::uint3(), v::string(n), r::bits>>, size, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_bit(v, size) | acc]) - defp decode_bit(<<0xFE, n::uint8, v::string(n), r::bits>>, size, null_bitmap, t, acc), + defp decode_bit(<<0xFE, n::uint8(), v::string(n), r::bits>>, size, null_bitmap, t, acc), do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_bit(v, size) | acc]) defp decode_bit(binary, size) do diff --git a/lib/myxql/query.ex b/lib/myxql/query.ex index 5af2157..cbc820a 100644 --- a/lib/myxql/query.ex +++ b/lib/myxql/query.ex @@ -1,6 +1,46 @@ defmodule MyXQL.Query do @moduledoc """ - Query struct returned from a successfully prepared query. + A struct for a prepared statement that returns a single result. + + For the struct returned from a query that returns multiple + results, see `MyXQL.Queries`. + + Its public fields are: + + * `:name` - The name of the prepared statement; + * `:num_params` - The number of parameter placeholders; + * `:statement` - The prepared statement + + ## Named and Unnamed Queries + + Named queries are identified by the non-empty value in `:name` field + and are meant to be re-used. + + Unnamed queries, with `:name` equal to `""`, are automatically closed + after being executed. + """ + + @type t :: %__MODULE__{ + name: iodata(), + cache: :reference | :statement, + num_params: non_neg_integer(), + statement: iodata() + } + + defstruct name: "", + cache: :reference, + num_params: nil, + ref: nil, + statement: nil, + statement_id: nil +end + +defmodule MyXQL.Queries do + @moduledoc """ + A struct for a prepared statement that returns multiple results. + + An example use case is a stored procedure with multiple `SELECT` + statements. Its public fields are: @@ -30,45 +70,45 @@ defmodule MyXQL.Query do ref: nil, statement: nil, statement_id: nil +end - defimpl DBConnection.Query do - def parse(query, _opts) do - query - end +defimpl DBConnection.Query, for: [MyXQL.Query, MyXQL.Queries] do + def parse(query, _opts) do + query + end - def describe(query, _opts) do - query - end + def describe(query, _opts) do + query + end - def encode(%{ref: nil} = query, _params, _opts) do - raise ArgumentError, "query #{inspect(query)} has not been prepared" - end + def encode(%{ref: nil} = query, _params, _opts) do + raise ArgumentError, "query #{inspect(query)} has not been prepared" + end - def encode(%{num_params: nil} = query, _params, _opts) do - raise ArgumentError, "query #{inspect(query)} has not been prepared" - end + def encode(%{num_params: nil} = query, _params, _opts) do + raise ArgumentError, "query #{inspect(query)} has not been prepared" + end - def encode(%{num_params: num_params} = query, params, _opts) - when num_params != length(params) do - message = - "expected params count: #{inspect(num_params)}, got values: #{inspect(params)}" <> - " for query: #{inspect(query)}" + def encode(%{num_params: num_params} = query, params, _opts) + when num_params != length(params) do + message = + "expected params count: #{inspect(num_params)}, got values: #{inspect(params)}" <> + " for query: #{inspect(query)}" - raise ArgumentError, message - end + raise ArgumentError, message + end - def encode(_query, params, _opts) do - MyXQL.Protocol.encode_params(params) - end + def encode(_query, params, _opts) do + MyXQL.Protocol.encode_params(params) + end - def decode(_query, result, _opts) do - result - end + def decode(_query, result, _opts) do + result end +end - defimpl String.Chars do - def to_string(%{statement: statement}) do - IO.iodata_to_binary(statement) - end +defimpl String.Chars, for: [MyXQL.Query, MyXQL.Queries] do + def to_string(%{statement: statement}) do + IO.iodata_to_binary(statement) end end diff --git a/lib/myxql/result.ex b/lib/myxql/result.ex index ac5655b..80893b0 100644 --- a/lib/myxql/result.ex +++ b/lib/myxql/result.ex @@ -40,3 +40,15 @@ defmodule MyXQL.Result do :num_warnings ] end + +if Code.ensure_loaded?(Table.Reader) do + defimpl Table.Reader, for: MyXQL.Result do + def init(%{columns: columns}) when columns in [nil, []] do + {:rows, %{columns: [], count: 0}, []} + end + + def init(result) do + {:rows, %{columns: result.columns, count: result.num_rows}, result.rows} + end + end +end diff --git a/lib/myxql/text_query.ex b/lib/myxql/text_query.ex index 3567250..f3ded10 100644 --- a/lib/myxql/text_query.ex +++ b/lib/myxql/text_query.ex @@ -2,24 +2,30 @@ defmodule MyXQL.TextQuery do @moduledoc false defstruct [:statement] +end - defimpl DBConnection.Query do - def parse(query, _opts), do: query +defmodule MyXQL.TextQueries do + @moduledoc false - def describe(query, _opts), do: query + defstruct [:statement] +end - def encode(_query, [], _opts), do: [] +defimpl DBConnection.Query, for: [MyXQL.TextQuery, MyXQL.TextQueries] do + def parse(query, _opts), do: query - def encode(_query, params, _opts) do - raise ArgumentError, "text queries cannot use parameters, got: #{inspect(params)}" - end + def describe(query, _opts), do: query - def decode(_query, result, _opts), do: result + def encode(_query, [], _opts), do: [] + + def encode(_query, params, _opts) do + raise ArgumentError, "text queries cannot use parameters, got: #{inspect(params)}" end - defimpl String.Chars do - def to_string(%{statement: statement}) do - IO.iodata_to_binary(statement) - end + def decode(_query, result, _opts), do: result +end + +defimpl String.Chars, for: [MyXQL.TextQuery, MyXQL.TextQueries] do + def to_string(%{statement: statement}) do + IO.iodata_to_binary(statement) end end diff --git a/mix.exs b/mix.exs index 460c34d..0e41588 100644 --- a/mix.exs +++ b/mix.exs @@ -1,14 +1,14 @@ defmodule MyXQL.MixProject do use Mix.Project - @version "0.5.1" + @version "0.6.3" @source_url "https://github.com/elixir-ecto/myxql" def project() do [ app: :myxql, version: @version, - elixir: "~> 1.6", + elixir: "~> 1.7", start_permanent: Mix.env() == :prod, name: "MyXQL", description: "MySQL 5.5+ driver for Elixir", @@ -45,10 +45,11 @@ defmodule MyXQL.MixProject do defp deps() do [ - {:db_connection, "~> 2.0", db_connection_opts()}, + {:db_connection, "~> 2.4.1 or ~> 2.5", db_connection_opts()}, {:decimal, "~> 1.6 or ~> 2.0"}, {:jason, "~> 1.0", optional: true}, {:geo, "~> 3.4", optional: true}, + {:table, "~> 0.1.0", optional: true}, {:binpp, ">= 0.0.0", only: [:dev, :test]}, {:dialyxir, "~> 1.0", only: :dev, runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 00dcfea..f13e9e5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,20 @@ %{ "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, "binpp": {:hex, :binpp, "1.1.1", "a01060124841ed3a22ed98ddeafc10d14166d2991683d5ce907a3a410559c9ee", [:rebar3], [], "hexpm", "2ef9fb04a1c7a79644c84e8402e1dc5a7f2bf2b182c211329465f3f188e923fa"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, - "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, + "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, + "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, "geo": {:hex, :geo, "3.4.0", "d592cf647fc9651ff32ca3467f385d88700d504de0ca6ee196a64f1a5200cba3", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6dd5fd42e5a51f270f209c8634eb63b3390bf9d03641442d61b38c41317eb1dc"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, + "table": {:hex, :table, "0.1.0", "f16104d717f960a623afb134a91339d40d8e11e0c96cfce54fee086b333e43f0", [:mix], [], "hexpm", "bf533d3606823ad8a7ee16f41941e5e6e0e42a20c4504cdf4cfabaaed1c8acb9"}, + "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, } diff --git a/test/myxql/client_test.exs b/test/myxql/client_test.exs index 00debcf..22af36b 100644 --- a/test/myxql/client_test.exs +++ b/test/myxql/client_test.exs @@ -366,9 +366,23 @@ defmodule MyXQL.ClientTest do Client.com_quit(client) end + + test "with stored procedure using a cursor", %{client: client} do + {:ok, com_stmt_prepare_ok(statement_id: statement_id)} = + Client.com_stmt_prepare(client, "CALL cursor_procedure()") + + {:ok, resultset(num_rows: 1, rows: [[3]])} = + Client.com_stmt_execute(client, statement_id, [], :cursor_type_read_only) + + # This will be called if, for instance, someone issues the procedure statement from Ecto.Adapters.SQL.query + {:ok, resultset(num_rows: 1, rows: [[3]])} = + Client.com_stmt_execute(client, statement_id, [], :cursor_type_no_cursor) + + Client.com_quit(client) + end end - describe "recv_packets/4" do + describe "recv_packets/5" do test "simple" do %{port: port} = start_fake_server(fn %{accept_socket: sock} -> @@ -380,7 +394,7 @@ defmodule MyXQL.ClientTest do end {:ok, client} = Client.do_connect(Client.Config.new(port: port)) - assert Client.recv_packets(client, decoder, :initial) == {:ok, "foo"} + assert Client.recv_packets(client, decoder, :initial, :single) == {:ok, "foo"} end end diff --git a/test/myxql/protocol/types_test.exs b/test/myxql/protocol/types_test.exs index f92187a..ed0c57f 100644 --- a/test/myxql/protocol/types_test.exs +++ b/test/myxql/protocol/types_test.exs @@ -1,7 +1,6 @@ defmodule MyXQL.Protocol.TypesTest do use ExUnit.Case, async: true import MyXQL.Protocol.Types - use Bitwise test "int_lenenc" do assert decode_int_lenenc(<<100>>) == 100 diff --git a/test/myxql/protocol/values_test.exs b/test/myxql/protocol/values_test.exs index 1003c78..6ab9a60 100644 --- a/test/myxql/protocol/values_test.exs +++ b/test/myxql/protocol/values_test.exs @@ -1,6 +1,6 @@ defmodule MyXQL.Protocol.ValueTest do use ExUnit.Case, async: true - use Bitwise + import Bitwise @default_sql_mode "STRICT_TRANS_TABLES" diff --git a/test/myxql/sync_test.exs b/test/myxql/sync_test.exs index 62e6735..e17e46a 100644 --- a/test/myxql/sync_test.exs +++ b/test/myxql/sync_test.exs @@ -79,6 +79,17 @@ defmodule MyXQL.SyncTest do assert prepared_stmt_count() == 0 end + test "do not leak with single and multiple result queries using the same name" do + {:ok, conn} = MyXQL.start_link(@opts) + assert prepared_stmt_count() == 0 + + {:ok, _} = MyXQL.prepare_many(conn, "foo", "CALL multi_procedure()") + assert prepared_stmt_count() == 1 + + {:ok, _} = MyXQL.prepare(conn, "foo", "SELECT 42") + assert prepared_stmt_count() == 1 + end + defp prepared_stmt_count() do [%{"Value" => count}] = TestHelper.mysql!("show global status like 'Prepared_stmt_count'") String.to_integer(count) diff --git a/test/myxql_test.exs b/test/myxql_test.exs index b3ec109..f02f9ca 100644 --- a/test/myxql_test.exs +++ b/test/myxql_test.exs @@ -177,6 +177,56 @@ defmodule MyXQLTest do assert result.num_rows == num end end + + test "query_many/4 with text", c do + assert {:ok, [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}]} = + MyXQL.query_many(c.conn, "SELECT 1; SELECT 2", [], query_type: :text) + + assert {:ok, [%MyXQL.Result{rows: [[1]]}]} = + MyXQL.query_many(c.conn, "SELECT 1;", [], query_type: :text) + end + + test "query_many!/4 with text", c do + assert [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}] = + MyXQL.query_many!(c.conn, "SELECT 1; SELECT 2", [], query_type: :text) + + assert [%MyXQL.Result{rows: [[1]]}] = + MyXQL.query_many!(c.conn, "SELECT 1;", [], query_type: :text) + end + + test "table reader integration", c do + assert {:ok, result} = + MyXQL.query( + c.conn, + """ + SELECT 1 AS x, 'a' AS y + UNION + SELECT 2 AS x, 'b' AS y + UNION + SELECT 3 AS x, 'c' AS y + """, + [] + ) + + assert result |> Table.to_rows() |> Enum.to_list() == [ + %{"x" => 1, "y" => "a"}, + %{"x" => 2, "y" => "b"}, + %{"x" => 3, "y" => "c"} + ] + + columns = Table.to_columns(result) + assert Enum.to_list(columns["x"]) == [1, 2, 3] + assert Enum.to_list(columns["y"]) == ["a", "b", "c"] + + assert {_, %{count: 3}, _} = Table.Reader.init(result) + end + + test "query statement is printed in error", c do + {:error, e} = MyXQL.query(c.conn, "SELECT not_a_column FROM integers", []) + error_log = MyXQL.Error.message(e) + assert error_log =~ "query: SELECT not_a_column FROM integers" + assert error_log =~ "(1054) Unknown column" + end end describe ":prepare option" do @@ -614,21 +664,24 @@ defmodule MyXQLTest do assert %MyXQL.Result{rows: [[1]]} = MyXQL.query!(c.conn, "CALL single_procedure()", [], query_type: :text) - assert_raise RuntimeError, "returning multiple results is not yet supported", fn -> - assert %MyXQL.Result{rows: [[1]]} = MyXQL.query!(c.conn, "CALL multi_procedure()") - end + assert [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}] = + MyXQL.query_many!(c.conn, "CALL multi_procedure()") end test "prepared query", c do - assert {_, %MyXQL.Result{rows: [[1]]}} = + assert {%MyXQL.Query{}, %MyXQL.Result{rows: [[1]]}} = MyXQL.prepare_execute!(c.conn, "", "CALL single_procedure()") - assert {_, %MyXQL.Result{rows: [[1]]}} = + assert {%MyXQL.Query{}, %MyXQL.Result{rows: [[1]]}} = MyXQL.prepare_execute!(c.conn, "", "CALL single_procedure()") - assert_raise RuntimeError, "returning multiple results is not yet supported", fn -> - MyXQL.prepare_execute!(c.conn, "", "CALL multi_procedure()") - end + assert {%MyXQL.Queries{}, [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}]} = + MyXQL.prepare_execute_many!(c.conn, "", "CALL multi_procedure()") + + assert %MyXQL.Queries{} = query = MyXQL.prepare_many!(c.conn, "", "CALL multi_procedure()") + + assert [%MyXQL.Result{rows: [[1]]}, %MyXQL.Result{rows: [[2]]}] = + MyXQL.execute_many!(c.conn, query) end test "stream procedure with single result", c do @@ -646,7 +699,7 @@ defmodule MyXQLTest do test "stream procedure with multiple results", c do statement = "CALL multi_procedure()" - assert_raise RuntimeError, "returning multiple results is not yet supported", fn -> + assert_raise RuntimeError, ~r"returning multiple results is not supported", fn -> MyXQL.transaction(c.conn, fn conn -> stream = MyXQL.stream(conn, statement, [], max_rows: 2) Enum.to_list(stream) @@ -655,6 +708,73 @@ defmodule MyXQLTest do end end + describe "multiple results" do + setup :connect + + test "using query/4 with a multiple result query", c do + assert_raise RuntimeError, ~r"returning multiple results is not supported", fn -> + MyXQL.query(c.conn, "SELECT 1; SELECT 2;", [], query_type: :text) + end + end + + test "using prepare/4 with a multiple result query", c do + {:error, error} = MyXQL.prepare(c.conn, "foo", "SELECT 1; SELECT 2;") + assert error.message =~ "You have an error in your SQL syntax" + end + + test "using prepare_execute/4 with a multiple result query", c do + {:error, error} = MyXQL.prepare_execute(c.conn, "foo", "SELECT 1; SELECT 2;") + assert error.message =~ "You have an error in your SQL syntax" + end + + test "using execute/4 with a multiple result query", c do + %MyXQL.Queries{} = query = MyXQL.prepare_many!(c.conn, "", "CALL multi_procedure()") + + assert_raise FunctionClauseError, fn -> + MyXQL.execute(c.conn, query) + end + end + + test "using query_many/4 with a single result query", c do + assert {:ok, [%MyXQL.Result{rows: [[1]]}]} = + MyXQL.query_many(c.conn, "SELECT 1;", [], query_type: :text) + end + + test "using query_many/4 with a multiple result query that is not a stored procedure", c do + {:error, error} = MyXQL.query_many(c.conn, "SELECT 1; SELECT 2", [], query_type: :binary) + assert error.message =~ "You have an error in your SQL syntax" + end + + test "using prepare_many/4 with a multiple result query that is not a stored procedure", c do + {:error, error} = MyXQL.prepare_many(c.conn, "foo", "SELECT 1; SELECT 2;") + assert error.message =~ "You have an error in your SQL syntax" + end + + test "using prepare_execute_many/4 with a multiple result query that is not a stored procedure", + c do + {:error, error} = MyXQL.prepare_execute_many(c.conn, "foo", "SELECT 1; SELECT 2;") + assert error.message =~ "You have an error in your SQL syntax" + end + + test "using prepare_execute_many/4 with a single result query", c do + assert {:ok, %MyXQL.Queries{}, [%MyXQL.Result{rows: [[1]]}]} = + MyXQL.prepare_execute_many(c.conn, "foo", "SELECT 1;") + end + + test "using execute_many/4 with a single result query", c do + %MyXQL.Query{} = query = MyXQL.prepare!(c.conn, "", "CALL single_procedure()") + + assert_raise FunctionClauseError, fn -> + MyXQL.execute_many(c.conn, query) + end + end + + test "close a multiple result prepared statement", c do + assert %MyXQL.Queries{} = query = MyXQL.prepare_many!(c.conn, "", "CALL multi_procedure()") + assert :ok == MyXQL.close(c.conn, query) + end + end + @tag :skip describe "idle ping" do test "query before and after" do diff --git a/test/test_helper.exs b/test/test_helper.exs index bd819e6..9370cc3 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -156,6 +156,31 @@ defmodule TestHelper do SELECT 2; END$$ DELIMITER ; + + DROP PROCEDURE IF EXISTS cursor_procedure; + DELIMITER $$ + CREATE PROCEDURE cursor_procedure() + BEGIN + DECLARE finished BOOLEAN DEFAULT FALSE; + DECLARE test_var INT; + DECLARE test_cursor CURSOR FOR SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = TRUE; + + OPEN test_cursor; + + test_loop: LOOP + FETCH test_cursor INTO test_var; + + IF finished THEN + LEAVE test_loop; + END IF; + END LOOP; + + CLOSE test_cursor; + + SELECT test_var AS result; + END$$ + DELIMITER ; """) end