Skip to content

Commit de38918

Browse files
Add text query support (#761)
1 parent b3e895a commit de38918

4 files changed

Lines changed: 114 additions & 10 deletions

File tree

lib/postgrex.ex

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule Postgrex do
2323
you need to start a separate (notifications) connection.
2424
"""
2525

26-
alias Postgrex.Query
26+
alias Postgrex.{Query, TextQuery}
2727

2828
@typedoc """
2929
A connection process name, pid or reference.
@@ -252,12 +252,18 @@ defmodule Postgrex do
252252
end
253253

254254
@doc """
255-
Runs an (extended) query and returns the result as `{:ok, %Postgrex.Result{}}`
256-
or `{:error, %Postgrex.Error{}}` if there was a database error. Parameters can
257-
be set in the query as `$1` embedded in the query string. Parameters are given
258-
as a list of elixir values. See the README for information on how Postgrex
259-
encodes and decodes Elixir values by default. See `Postgrex.Result` for the
260-
result data.
255+
Runs a query and returns the result as `{:ok, %Postgrex.Result{}}` or
256+
`{:error, %Postgrex.Error{}}` if there was a database error.
257+
258+
Queries can be run using both the extended query protocol (binary format)
259+
or the simple query protocol (text format). This can be controlled using
260+
the `:query_type` option.
261+
262+
If using the extended query protocol, parameters can be set as `$1` embedded
263+
in the query string and results are encoded and decoded according to the
264+
[data representation chart](readme.html#data-representation). If using the
265+
simple query protocol, queries cannot be parameterized and results are encoded
266+
and decoded in text format. See `Postgrex.Result` for the result data.
261267
262268
This function may still raise an exception if there is an issue with types
263269
(`ArgumentError`), connection (`DBConnection.ConnectionError`), ownership
@@ -272,6 +278,9 @@ defmodule Postgrex do
272278
* `:mode` - set to `:savepoint` to use a savepoint to rollback to before the
273279
query on error, otherwise set to `:transaction` (default: `:transaction`);
274280
* `:cache_statement` - Caches the query with the given name
281+
* `:query_type` - Either `:binary` or `:text`. If `:binary` then the
282+
extended query protocol is used. If `:text` then the simple protocol
283+
is used. Defaults to `:binary`.
275284
276285
## Examples
277286
@@ -290,6 +299,22 @@ defmodule Postgrex do
290299
@spec query(conn, iodata, list, [execute_option]) ::
291300
{:ok, Postgrex.Result.t()} | {:error, Exception.t()}
292301
def query(conn, statement, params \\ [], opts \\ []) when is_list(params) and is_list(opts) do
302+
query_type = Keyword.get(opts, :query_type, :binary)
303+
304+
case query_type do
305+
:binary ->
306+
binary_query(conn, statement, params, opts)
307+
308+
:text ->
309+
text_query(conn, statement, params, opts)
310+
311+
_ ->
312+
raise ArgumentError,
313+
"allowed query types are `:binary` and `:text`, got: #{inspect(query_type)}"
314+
end
315+
end
316+
317+
defp binary_query(conn, statement, params, opts) do
293318
name = Keyword.get(opts, :cache_statement)
294319

295320
if comment_not_present!(opts) && name do
@@ -315,6 +340,15 @@ defmodule Postgrex do
315340
end
316341
end
317342

343+
defp text_query(conn, statement, params, opts) do
344+
query = %TextQuery{statement: statement}
345+
346+
case DBConnection.execute(conn, query, params, opts) do
347+
{:ok, _, result} -> {:ok, result}
348+
{:error, _} = error -> error
349+
end
350+
end
351+
318352
defp query_prepare_execute(conn, query, params, opts) do
319353
case DBConnection.prepare_execute(conn, query, params, opts) do
320354
{:ok, _, result} -> {:ok, result}

lib/postgrex/protocol.ex

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Postgrex.Protocol do
22
@moduledoc false
33

4-
alias Postgrex.{Types, TypeServer, Query, Cursor, Copy}
4+
alias Postgrex.{Types, TypeServer, Query, TextQuery, Cursor, Copy}
55
import Postgrex.{Messages, BinaryUtils}
66
require Logger
77
use DBConnection
@@ -55,6 +55,8 @@ defmodule Postgrex.Protocol do
5555

5656
@type notify :: (binary, binary -> any)
5757

58+
@type binary_or_text_query :: Postgrex.Query.t() | Postgrex.TextQuery.t()
59+
5860
defmacrop new_status(opts, fields \\ []) do
5961
defaults =
6062
quote(
@@ -421,8 +423,9 @@ defmodule Postgrex.Protocol do
421423
end
422424
end
423425

424-
@spec handle_execute(Postgrex.Query.t(), list, Keyword.t(), state) ::
425-
{:ok, Postgrex.Query.t(), Postgrex.Result.t() | Postgrex.Copy.t(), state}
426+
@spec handle_execute(binary_or_text_query, list, Keyword.t(), state) ::
427+
{:ok, binary_or_text_query,
428+
Postgrex.Result.t() | [Postgrex.Result.t()] | Postgrex.Copy.t(), state}
426429
| {:error, %ArgumentError{} | Postgrex.Error.t(), state}
427430
| {:error, %DBConnection.TransactionError{}, state}
428431
| {:disconnect, %RuntimeError{}, state}
@@ -437,6 +440,16 @@ defmodule Postgrex.Protocol do
437440
end
438441
end
439442

443+
def handle_execute(%TextQuery{statement: statement} = query, [], opts, s) do
444+
case handle_simple(statement, opts, s) do
445+
{:ok, [first_result | _], s} ->
446+
{:ok, query, first_result, s}
447+
448+
{error, _, _} = other when error in [:error, :disconnect] ->
449+
other
450+
end
451+
end
452+
440453
@spec handle_execute(Postgrex.Copy.t(), {:copy_data, iodata} | :copy_done, Keyword.t(), state) ::
441454
{:ok, Postgrex.Query.t(), Postgrex.Result.t(), state}
442455
| {:error, %ArgumentError{} | Postgrex.Error.t(), state}

lib/postgrex/text_query.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule Postgrex.TextQuery do
2+
@moduledoc false
3+
4+
defstruct [:statement]
5+
end
6+
7+
defimpl DBConnection.Query, for: Postgrex.TextQuery do
8+
def parse(query, _opts), do: query
9+
10+
def describe(query, _opts), do: query
11+
12+
def encode(_query, [], _opts), do: []
13+
14+
def encode(_query, params, _opts) do
15+
raise ArgumentError, "text queries cannot use parameters, got: #{inspect(params)}"
16+
end
17+
18+
def decode(_query, result, _opts), do: result
19+
end
20+
21+
defimpl String.Chars, for: Postgrex.TextQuery do
22+
def to_string(%{statement: statement}) do
23+
IO.iodata_to_binary(statement)
24+
end
25+
end

test/query_test.exs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,38 @@ defmodule QueryTest do
20692069
assert {:ok, _, _} = P.execute(pid, query, [])
20702070
end
20712071

2072+
test "query with query_type: text", context do
2073+
{:ok, %{rows: result}} =
2074+
P.query(context[:pid], "SELECT * FROM UNNEST(ARRAY[1, 2], ARRAY[3, 4])", [],
2075+
query_type: :text
2076+
)
2077+
2078+
assert result == [["1", "3"], ["2", "4"]]
2079+
2080+
%{rows: result} =
2081+
P.query!(context[:pid], "SELECT * FROM UNNEST(ARRAY[1, 2], ARRAY[3, 4])", [],
2082+
query_type: :text
2083+
)
2084+
2085+
assert result == [["1", "3"], ["2", "4"]]
2086+
end
2087+
2088+
test "query with query_type: :text only returns first result", context do
2089+
{:ok, %{rows: result}} =
2090+
P.query(context[:pid], "SELECT 1; SELECT 2", [], query_type: :text)
2091+
2092+
assert result == [["1"]]
2093+
end
2094+
2095+
test "query with query_type: :text handles errors properly", context do
2096+
{:error, %Postgrex.Error{}} =
2097+
P.query(context[:pid], "SELEC;", [], query_type: :text)
2098+
2099+
assert_raise Postgrex.Error, fn ->
2100+
P.query!(context[:pid], "SELEC;", [], query_type: :text)
2101+
end
2102+
end
2103+
20722104
defp disconnect(pid) do
20732105
sock = DBConnection.run(pid, &get_socket/1)
20742106
:gen_tcp.shutdown(sock, :read_write)

0 commit comments

Comments
 (0)