From fc5758fa4133d7eedc19fe2b53a4a9bd6108645e Mon Sep 17 00:00:00 2001
From: David Kumru <davidkumru@gmail.com>
Date: Fri, 13 Oct 2023 09:05:53 +0200
Subject: [PATCH 1/2] upgrade version

---
 .github/workflows/ci.yml                 |  26 +-
 CHANGELOG.md                             |  26 ++
 README.md                                |   4 +-
 lib/myxql.ex                             | 292 ++++++++++++++++++++---
 lib/myxql/client.ex                      |  59 +++--
 lib/myxql/connection.ex                  |  99 +++++---
 lib/myxql/error.ex                       |  21 +-
 lib/myxql/protocol.ex                    | 107 +++++----
 lib/myxql/protocol/flags.ex              |   2 +-
 lib/myxql/protocol/server_error_codes.ex |  13 +-
 lib/myxql/protocol/types.ex              |  14 +-
 lib/myxql/protocol/values.ex             |  95 ++++----
 lib/myxql/query.ex                       | 102 +++++---
 lib/myxql/result.ex                      |  12 +
 lib/myxql/text_query.ex                  |  30 ++-
 mix.exs                                  |   7 +-
 mix.lock                                 |  15 +-
 test/myxql/client_test.exs               |  18 +-
 test/myxql/protocol/types_test.exs       |   1 -
 test/myxql/protocol/values_test.exs      |   2 +-
 test/myxql/sync_test.exs                 |  11 +
 test/myxql_test.exs                      | 138 ++++++++++-
 test/test_helper.exs                     |  25 ++
 23 files changed, 856 insertions(+), 263 deletions(-)

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(
-         <<size::uint3, _seq::uint1, payload::string(size), rest::binary>>,
+         <<size::uint3(), _seq::uint1(), payload::string(size), rest::binary>> = data,
+         decoder,
+         {:more_results, resultset},
+         result_state,
+         timeout,
+         client,
+         partial
+       )
+       when size < @default_max_packet_size do
+    case decode_more_results(<<partial::binary, payload::binary>>, 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(
+         <<size::uint3(), _seq::uint1(), payload::string(size), rest::binary>>,
          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.(<<partial::binary, payload::binary>>, 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(
-         <<size::uint3, _seq::uint1, payload::string(size), rest::binary>>,
+         <<size::uint3(), _seq::uint1(), payload::string(size), rest::binary>>,
          decoder,
          decoder_state,
+         result_state,
          timeout,
          client,
          partial
@@ -228,6 +255,7 @@ defmodule MyXQL.Client do
       rest,
       decoder,
       decoder_state,
+      result_state,
       timeout,
       client,
       <<partial::binary, payload::binary>>
@@ -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(
           <<rest::binary, data::binary>>,
           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_size::uint3, sequence_id::uint1>>, payload]
+      [<<payload_size::uint3(), sequence_id::uint1()>>, 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(
-         <<code::uint2, _sql_state_marker::string(1), _sql_state::string(5), message::bits>>
+         <<code::uint2(), _sql_state_marker::string(1), _sql_state::string(5), message::bits>>
        ) 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(<<num_warnings::uint2, status_flags::uint2>>) do
+  defp decode_eof_packet_body(<<num_warnings::uint2(), status_flags::uint2()>>) do
     eof_packet(
       status_flags: status_flags,
       num_warnings: num_warnings
     )
   end
 
-  defp decode_connect_err_packet_body(<<code::uint2, message::bits>>) do
+  defp decode_connect_err_packet_body(<<code::uint2(), message::bits>>) 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
 
-    <<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
+    <<capability_flags::uint4()>> = <<capability_flags1::uint2(), capability_flags2::uint2()>>
     # 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: <<database::binary, 0x00>>, else: ""
 
     <<
-      capability_flags::uint4,
-      max_packet_size::uint4,
+      capability_flags::uint4(),
+      max_packet_size::uint4(),
       charset,
       0::uint(23),
       <<username::binary, 0x00>>,
@@ -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, <<statement_id::uint4>>]
+    [0x19, <<statement_id::uint4()>>]
   end
 
   # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html
   def encode_com({:com_stmt_reset, statement_id}) do
-    [0x1A, <<statement_id::uint4>>]
+    [0x1A, <<statement_id::uint4()>>]
   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
 
-    <<null_bitmap::uint(null_bitmap_size), new_params_bound_flag::uint1, types::binary,
+    <<null_bitmap::uint(null_bitmap_size), new_params_bound_flag::uint1(), types::binary,
       values::binary>>
   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: <<int>>
-  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(<<int::uint1, rest::binary>>) 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(<<int::uint1(), rest::binary>>) 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, <<value::int8>>}
+    {:mysql_type_longlong, <<value::int8()>>}
   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(<<srid::uint4, binary::binary>>)}
+      {:mysql_type_var_string, encode_string_lenenc(<<srid::uint4(), binary::binary>>)}
     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(<<srid::uint4, r::bits>>) do
+    defp decode_geometry(<<srid::uint4(), r::bits>>) 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(<<v::int1, r::bits>>, null_bitmap, t, acc),
+  defp decode_int1(<<v::int1(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_uint1(<<v::uint1, r::bits>>, null_bitmap, t, acc),
+  defp decode_uint1(<<v::uint1(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_int2(<<v::int2, r::bits>>, null_bitmap, t, acc),
+  defp decode_int2(<<v::int2(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_uint2(<<v::uint2, r::bits>>, null_bitmap, t, acc),
+  defp decode_uint2(<<v::uint2(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_int4(<<v::int4, r::bits>>, null_bitmap, t, acc),
+  defp decode_int4(<<v::int4(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_uint4(<<v::uint4, r::bits>>, null_bitmap, t, acc),
+  defp decode_uint4(<<v::uint4(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_int8(<<v::int8, r::bits>>, null_bitmap, t, acc),
+  defp decode_int8(<<v::int8(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
-  defp decode_uint8(<<v::uint8, r::bits>>, null_bitmap, t, acc),
+  defp decode_uint8(<<v::uint8(), r::bits>>, null_bitmap, t, acc),
     do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
 
   defp decode_float(<<v::32-signed-little-float, r::bits>>, 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(<<n::uint1, string::string(n), r::bits>>, null_bitmap, t, acc),
+  defp decode_decimal(<<n::uint1(), string::string(n), r::bits>>, 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(<<n::uint1, v::string(n), r::bits>>, null_bitmap, t, acc, decoder)
+  defp decode_string_lenenc(<<n::uint1(), v::string(n), r::bits>>, 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(<<n::uint1, v::string(n), r::bits>>, null_bitmap, t, acc) when n < 251,
+  defp decode_json(<<n::uint1(), v::string(n), r::bits>>, 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(<<n::uint1, v::string(n), r::bits>>, size, null_bitmap, t, acc) when n < 251,
+  defp decode_bit(<<n::uint1(), v::string(n), r::bits>>, 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
 

From 7e58f9c52aec679d8237a8cfc85696a193344a03 Mon Sep 17 00:00:00 2001
From: David Kumru <davidkumru@gmail.com>
Date: Fri, 13 Oct 2023 09:14:18 +0200
Subject: [PATCH 2/2] remove unused capability

---
 lib/myxql/protocol.ex | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/myxql/protocol.ex b/lib/myxql/protocol.ex
index f74f95b..4ed1e80 100644
--- a/lib/myxql/protocol.ex
+++ b/lib/myxql/protocol.ex
@@ -159,7 +159,6 @@ defmodule MyXQL.Protocol do
       put_capability_flags([
         :client_protocol_41,
         :client_secure_connection,
-        :client_multi_statements,
         # set by servers since 4.0
         :client_transactions
       ])