diff --git a/lib/postgrex.ex b/lib/postgrex.ex index 175e9992..1f2f68af 100644 --- a/lib/postgrex.ex +++ b/lib/postgrex.ex @@ -23,7 +23,7 @@ defmodule Postgrex do you need to start a separate (notifications) connection. """ - alias Postgrex.Query + alias Postgrex.{Query, TextQuery} @typedoc """ A connection process name, pid or reference. @@ -252,12 +252,18 @@ defmodule Postgrex do end @doc """ - Runs an (extended) query and returns the result as `{:ok, %Postgrex.Result{}}` - or `{:error, %Postgrex.Error{}}` if there was a database error. Parameters can - be set in the query as `$1` embedded in the query string. Parameters are given - as a list of elixir values. See the README for information on how Postgrex - encodes and decodes Elixir values by default. See `Postgrex.Result` for the - result data. + Runs a query and returns the result as `{:ok, %Postgrex.Result{}}` or + `{:error, %Postgrex.Error{}}` if there was a database error. + + Queries can be run using both the extended query protocol (binary format) + or the simple query protocol (text format). This can be controlled using + the `:query_type` option. + + If using the extended query protocol, parameters can be set as `$1` embedded + in the query string and results are encoded and decoded according to the + [data representation chart](readme.html#data-representation). If using the + simple query protocol, queries cannot be parameterized and results are encoded + and decoded in text format. See `Postgrex.Result` for the result data. This function may still raise an exception if there is an issue with types (`ArgumentError`), connection (`DBConnection.ConnectionError`), ownership @@ -272,6 +278,9 @@ defmodule Postgrex do * `:mode` - set to `:savepoint` to use a savepoint to rollback to before the query on error, otherwise set to `:transaction` (default: `:transaction`); * `:cache_statement` - Caches the query with the given name + * `:query_type` - Either `:binary` or `:text`. If `:binary` then the + extended query protocol is used. If `:text` then the simple protocol + is used. Defaults to `:binary`. ## Examples @@ -290,6 +299,22 @@ defmodule Postgrex do @spec query(conn, iodata, list, [execute_option]) :: {:ok, Postgrex.Result.t()} | {:error, Exception.t()} def query(conn, statement, params \\ [], opts \\ []) when is_list(params) and is_list(opts) do + query_type = Keyword.get(opts, :query_type, :binary) + + case query_type do + :binary -> + binary_query(conn, statement, params, opts) + + :text -> + text_query(conn, statement, params, opts) + + _ -> + raise ArgumentError, + "allowed query types are `:binary` and `:text`, got: #{inspect(query_type)}" + end + end + + defp binary_query(conn, statement, params, opts) do name = Keyword.get(opts, :cache_statement) if comment_not_present!(opts) && name do @@ -315,6 +340,15 @@ defmodule Postgrex do end end + defp text_query(conn, statement, params, opts) do + query = %TextQuery{statement: statement} + + case DBConnection.execute(conn, query, params, opts) do + {:ok, _, result} -> {:ok, result} + {:error, _} = error -> error + end + end + defp query_prepare_execute(conn, query, params, opts) do case DBConnection.prepare_execute(conn, query, params, opts) do {:ok, _, result} -> {:ok, result} diff --git a/lib/postgrex/protocol.ex b/lib/postgrex/protocol.ex index 31d321a4..7d815471 100644 --- a/lib/postgrex/protocol.ex +++ b/lib/postgrex/protocol.ex @@ -1,7 +1,7 @@ defmodule Postgrex.Protocol do @moduledoc false - alias Postgrex.{Types, TypeServer, Query, Cursor, Copy} + alias Postgrex.{Types, TypeServer, Query, TextQuery, Cursor, Copy} import Postgrex.{Messages, BinaryUtils} require Logger use DBConnection @@ -55,6 +55,8 @@ defmodule Postgrex.Protocol do @type notify :: (binary, binary -> any) + @type binary_or_text_query :: Postgrex.Query.t() | Postgrex.TextQuery.t() + defmacrop new_status(opts, fields \\ []) do defaults = quote( @@ -421,8 +423,9 @@ defmodule Postgrex.Protocol do end end - @spec handle_execute(Postgrex.Query.t(), list, Keyword.t(), state) :: - {:ok, Postgrex.Query.t(), Postgrex.Result.t() | Postgrex.Copy.t(), state} + @spec handle_execute(binary_or_text_query, list, Keyword.t(), state) :: + {:ok, binary_or_text_query, + Postgrex.Result.t() | [Postgrex.Result.t()] | Postgrex.Copy.t(), state} | {:error, %ArgumentError{} | Postgrex.Error.t(), state} | {:error, %DBConnection.TransactionError{}, state} | {:disconnect, %RuntimeError{}, state} @@ -437,6 +440,16 @@ defmodule Postgrex.Protocol do end end + def handle_execute(%TextQuery{statement: statement} = query, [], opts, s) do + case handle_simple(statement, opts, s) do + {:ok, [first_result | _], s} -> + {:ok, query, first_result, s} + + {error, _, _} = other when error in [:error, :disconnect] -> + other + end + end + @spec handle_execute(Postgrex.Copy.t(), {:copy_data, iodata} | :copy_done, Keyword.t(), state) :: {:ok, Postgrex.Query.t(), Postgrex.Result.t(), state} | {:error, %ArgumentError{} | Postgrex.Error.t(), state} diff --git a/lib/postgrex/text_query.ex b/lib/postgrex/text_query.ex new file mode 100644 index 00000000..7dc7869c --- /dev/null +++ b/lib/postgrex/text_query.ex @@ -0,0 +1,25 @@ +defmodule Postgrex.TextQuery do + @moduledoc false + + defstruct [:statement] +end + +defimpl DBConnection.Query, for: Postgrex.TextQuery do + def parse(query, _opts), do: query + + def describe(query, _opts), do: query + + def encode(_query, [], _opts), do: [] + + def encode(_query, params, _opts) do + raise ArgumentError, "text queries cannot use parameters, got: #{inspect(params)}" + end + + def decode(_query, result, _opts), do: result +end + +defimpl String.Chars, for: Postgrex.TextQuery do + def to_string(%{statement: statement}) do + IO.iodata_to_binary(statement) + end +end diff --git a/test/query_test.exs b/test/query_test.exs index ba8ae7c9..2820627a 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -2069,6 +2069,38 @@ defmodule QueryTest do assert {:ok, _, _} = P.execute(pid, query, []) end + test "query with query_type: text", context do + {:ok, %{rows: result}} = + P.query(context[:pid], "SELECT * FROM UNNEST(ARRAY[1, 2], ARRAY[3, 4])", [], + query_type: :text + ) + + assert result == [["1", "3"], ["2", "4"]] + + %{rows: result} = + P.query!(context[:pid], "SELECT * FROM UNNEST(ARRAY[1, 2], ARRAY[3, 4])", [], + query_type: :text + ) + + assert result == [["1", "3"], ["2", "4"]] + end + + test "query with query_type: :text only returns first result", context do + {:ok, %{rows: result}} = + P.query(context[:pid], "SELECT 1; SELECT 2", [], query_type: :text) + + assert result == [["1"]] + end + + test "query with query_type: :text handles errors properly", context do + {:error, %Postgrex.Error{}} = + P.query(context[:pid], "SELEC;", [], query_type: :text) + + assert_raise Postgrex.Error, fn -> + P.query!(context[:pid], "SELEC;", [], query_type: :text) + end + end + defp disconnect(pid) do sock = DBConnection.run(pid, &get_socket/1) :gen_tcp.shutdown(sock, :read_write)