From 99d8e0c72992ee1eb8551a034cc8ae13af095651 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Fri, 30 Jan 2026 19:20:32 -0500 Subject: [PATCH 1/5] support text queries --- .github/workflows/ci.yml | 1 + lib/postgrex.ex | 48 ++++++++++++++++++++++++++++++++++------ lib/postgrex/protocol.ex | 30 ++++++++++++++++++++++--- test/query_test.exs | 32 +++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f02e8346..bd099688 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - text_query pull_request: jobs: 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..31ebc100 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, TextQueries, Cursor, Copy} import Postgrex.{Messages, BinaryUtils} require Logger use DBConnection @@ -55,6 +55,9 @@ defmodule Postgrex.Protocol do @type notify :: (binary, binary -> any) + @type binary_or_text_query :: + Postgrex.Query.t() | Postgrex.TextQuery.t() | Postgrex.TextQueries.t() + defmacrop new_status(opts, fields \\ []) do defaults = quote( @@ -421,8 +424,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 +441,26 @@ 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 + + def handle_execute(%TextQueries{statement: statement} = query, [], opts, s) do + case handle_simple(statement, opts, s) do + {:ok, results, s} -> + {:ok, query, results, 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/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) From 1eb67f7dd5199516e9749aa3a24c4caa001bbbfc Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Fri, 30 Jan 2026 19:22:08 -0500 Subject: [PATCH 2/5] add new structs --- lib/postgrex/text_query.ex | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/postgrex/text_query.ex diff --git a/lib/postgrex/text_query.ex b/lib/postgrex/text_query.ex new file mode 100644 index 00000000..41677c5b --- /dev/null +++ b/lib/postgrex/text_query.ex @@ -0,0 +1,31 @@ +defmodule Postgrex.TextQuery do + @moduledoc false + + defstruct [:statement] +end + +defmodule Postgrex.TextQueries do + @moduledoc false + + defstruct [:statement] +end + +defimpl DBConnection.Query, for: [Postgrex.TextQuery, Postgrex.TextQueries] 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, Postgrex.TextQuery] do + def to_string(%{statement: statement}) do + IO.iodata_to_binary(statement) + end +end From a52ff2b9d109bf0728cbf912b492e14436b45667 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Fri, 30 Jan 2026 19:25:03 -0500 Subject: [PATCH 3/5] put ci back to normal --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd099688..f02e8346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - text_query pull_request: jobs: From f4d939f0f7c1076bdce363f399aaa04124ca7e43 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Fri, 30 Jan 2026 19:31:57 -0500 Subject: [PATCH 4/5] oops --- lib/postgrex/text_query.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/postgrex/text_query.ex b/lib/postgrex/text_query.ex index 41677c5b..07b82381 100644 --- a/lib/postgrex/text_query.ex +++ b/lib/postgrex/text_query.ex @@ -24,7 +24,7 @@ defimpl DBConnection.Query, for: [Postgrex.TextQuery, Postgrex.TextQueries] do def decode(_query, result, _opts), do: result end -defimpl String.Chars, for: [Postgrex.TextQuery, Postgrex.TextQuery] do +defimpl String.Chars, for: [Postgrex.TextQuery, Postgrex.TextQueries] do def to_string(%{statement: statement}) do IO.iodata_to_binary(statement) end From 70fb546b7fb9ac4b1658d5d14c4e41021d433e76 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Sun, 1 Feb 2026 14:15:03 -0500 Subject: [PATCH 5/5] get rid of textqueries for now --- lib/postgrex/protocol.ex | 15 ++------------- lib/postgrex/text_query.ex | 10 ++-------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/lib/postgrex/protocol.ex b/lib/postgrex/protocol.ex index 31ebc100..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, TextQuery, TextQueries, Cursor, Copy} + alias Postgrex.{Types, TypeServer, Query, TextQuery, Cursor, Copy} import Postgrex.{Messages, BinaryUtils} require Logger use DBConnection @@ -55,8 +55,7 @@ defmodule Postgrex.Protocol do @type notify :: (binary, binary -> any) - @type binary_or_text_query :: - Postgrex.Query.t() | Postgrex.TextQuery.t() | Postgrex.TextQueries.t() + @type binary_or_text_query :: Postgrex.Query.t() | Postgrex.TextQuery.t() defmacrop new_status(opts, fields \\ []) do defaults = @@ -451,16 +450,6 @@ defmodule Postgrex.Protocol do end end - def handle_execute(%TextQueries{statement: statement} = query, [], opts, s) do - case handle_simple(statement, opts, s) do - {:ok, results, s} -> - {:ok, query, results, 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 index 07b82381..7dc7869c 100644 --- a/lib/postgrex/text_query.ex +++ b/lib/postgrex/text_query.ex @@ -4,13 +4,7 @@ defmodule Postgrex.TextQuery do defstruct [:statement] end -defmodule Postgrex.TextQueries do - @moduledoc false - - defstruct [:statement] -end - -defimpl DBConnection.Query, for: [Postgrex.TextQuery, Postgrex.TextQueries] do +defimpl DBConnection.Query, for: Postgrex.TextQuery do def parse(query, _opts), do: query def describe(query, _opts), do: query @@ -24,7 +18,7 @@ defimpl DBConnection.Query, for: [Postgrex.TextQuery, Postgrex.TextQueries] do def decode(_query, result, _opts), do: result end -defimpl String.Chars, for: [Postgrex.TextQuery, Postgrex.TextQueries] do +defimpl String.Chars, for: Postgrex.TextQuery do def to_string(%{statement: statement}) do IO.iodata_to_binary(statement) end