Packages
postgrex
0.11.0
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.13
0.15.12
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.13.0-rc.0
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
PostgreSQL driver for Elixir
Current section
Files
Jump to
Current section
Files
lib/postgrex/protocol.ex
defmodule Postgrex.Protocol do
@moduledoc false
alias Postgrex.Types
alias Postgrex.Query
import Postgrex.Messages
import Postgrex.BinaryUtils
require Logger
@timeout 5000
@sock_opts [packet: :raw, mode: :binary, active: false]
defstruct [sock: nil, connection_id: nil, types: nil, timeout: nil,
parameters: %{}, queries: nil, postgres: :idle, buffer: nil]
@type state :: %__MODULE__{sock: {module, any},
connection_id: pos_integer,
types: (nil | reference | Postgrex.TypeServer.table),
timeout: timeout,
parameters: %{binary => binary} | reference,
queries: nil | :ets.tid,
postgres: :idle | :transaction | :naive,
buffer: nil | binary | :active_once}
@type notify :: ((binary, binary) -> any)
@reserved_prefix "POSTGREX_"
@reserved_queries ["BEGIN",
"COMMIT",
"ROLLBACK",
"SAVEPOINT postgrex_savepoint",
"RELEASE SAVEPOINT postgrex_savepoint",
"ROLLBACK TO SAVEPOINT postgrex_savepoint"]
@spec connect(Keyword.t) ::
{:ok, state} | {:error, Postgrex.Error.t}
def connect(opts) do
host = Keyword.fetch!(opts, :hostname) |> to_char_list
port = opts[:port] || 5432
timeout = opts[:timeout] || @timeout
sock_opts = [send_timeout: timeout] ++ (opts[:socket_options] || [])
custom = opts[:extensions] || []
extensions = custom ++ Postgrex.Utils.default_extensions()
ssl? = opts[:ssl] || false
types? = Keyword.fetch!(opts, :types)
postgres =
case opts[:transactions] || :naive do
:naive -> :naive
:strict -> :idle
end
s = %__MODULE__{timeout: timeout, postgres: postgres}
types_key = if types?, do: {host, port, Keyword.fetch!(opts, :database), custom}
status = %{opts: opts, types_key: types_key, types_ref: nil,
types_table: nil, extensions: extensions, extension_info: nil}
case connect(host, port, sock_opts ++ @sock_opts, s) do
{:ok, s} when ssl? -> s |> ssl(status) |> connected()
{:ok, s} -> s |> startup(status) |> connected()
{:error, _} = error -> error
end
end
defp connected({:ok, %{parameters: parameters} = s}) do
ref = Postgrex.Parameters.insert(parameters)
tid = queries_new()
{:ok, %{s | parameters: ref, queries: tid}}
end
defp connected({:disconnect, err, s}) do
disconnect(err, s)
{:error, err}
end
@spec disconnect(Exception.t, state) :: :ok
def disconnect(err, %{types: ref}) when is_reference(ref) do
# Don't handle the case where connection failure occurs during bootstrap
# (hard to test and "unlikely" given auth just succeeded)
raise err
end
def disconnect(_, s) do
sock_close(s)
_ = recv_buffer(s)
delete_parameters(s)
queries_delete(s)
:ok
end
@spec ping(state) ::
{:ok, state} | {:disconnect, Postgrex.Error.t, state}
def ping(%{buffer: buffer} = s) do
status = %{notify: notify([]), sync: :sync}
s = %{s | buffer: nil}
case buffer do
:active_once ->
sync(s, status, :active_once, buffer)
_ when is_binary(buffer) ->
sync(s, status, nil, buffer)
end
end
@spec checkout(state) ::
{:ok, state} | {:disconnect, Postgrex.Error.t, state}
def checkout(%{postgres: :transaction} = s) do
sync_error(s, :transaction)
end
def checkout(%{buffer: :active_once} = s) do
case setopts(s, [active: :false], :active_once) do
:ok -> recv_buffer(s)
{:disconnect, _, _} = dis -> dis
end
end
@spec checkin(state) ::
{:ok, state} | {:disconnect, Postgrex.Error.t, state}
def checkin(%{postgres: :transaction} = s) do
sync_error(s, :transaction)
end
def checkin(%{buffer: buffer} = s) when is_binary(buffer) do
activate(s, buffer)
end
@spec handle_prepare(Postgrex.Query.t, Keyword.t, state) ::
{:ok, Postgrex.Query.t, state} |
{:error, ArgumentError.t, state} |
{:error | :disconnect, Postgrex.Query.t, state}
def handle_prepare(%Query{name: @reserved_prefix <> _} = query, _, s) do
reserved_error(query, s)
end
def handle_prepare(%Query{types: nil} = query, opts, %{buffer: buffer} = s) do
case query_member?(s, query) do
true ->
status = %{notify: notify(opts), sync: :sync, prepare: :describe}
describe_send(%{s | buffer: nil}, status, query, buffer)
false ->
status = %{notify: notify(opts), sync: :sync, prepare: :parse_describe}
parse_describe_send(%{s | buffer: nil}, status, query, buffer)
end
end
def handle_prepare(%Query{types: types} = query, _, %{types: types} = s) do
query_error(s, "query #{inspect query} has already been prepared")
end
def handle_prepare(%Query{} = query, _, s) do
query_error(s, "query #{inspect query} has invalid types for the connection")
end
@spec handle_execute(Postgrex.Query.t, list, Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error, ArgumentError.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_execute(%Query{} = query, params, opts, s) do
handle_execute(query, params, :sync, opts, s)
end
@spec handle_execute(Postgrex.Parameters.t, nil, Keyword.t, state) ::
{:ok, %{binary => binary}, state} |
{:error, Postgrex.Errpr.t, state}
def handle_execute(%Postgrex.Parameters{}, nil, _, s) do
%{parameters: parameters} = s
case Postgrex.Parameters.fetch(parameters) do
{:ok, parameters} ->
{:ok, parameters, s}
:error ->
{:error, %Postgrex.Error{message: "parameters not available"}, s}
end
end
@spec handle_execute_close(Postgrex.Query.t, list, Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error, ArgumentError.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_execute_close(%Query{name: @reserved_prefix <> _} = query, _, _, s) do
reserved_error(query, s)
end
def handle_execute_close(query, params, opts, s) do
handle_execute(query, params, :sync_close, opts, s)
end
@spec handle_close(Postgrex.Query.t, Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error, ArgumentError.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_close(%Query{name: @reserved_prefix <> _} = query, _, s) do
reserved_error(query, s)
end
def handle_close(query, opts, s) do
%{connection_id: connection_id, buffer: buffer} = s
status = %{notify: notify(opts)}
res = %Postgrex.Result{command: :close, connection_id: connection_id}
close(%{s | buffer: nil}, status, query, res, buffer)
end
@spec handle_begin(Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_begin(opts, s) do
case Keyword.get(opts, :mode, :transaction) do
:transaction ->
name = @reserved_prefix <> "BEGIN"
handle_transaction(name, :transaction, :begin, opts, s)
:savepoint ->
name = @reserved_prefix <> "SAVEPOINT postgrex_savepoint"
handle_savepoint([name], :savepoint, opts, s)
end
end
@spec handle_commit(Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_commit(opts, s) do
case Keyword.get(opts, :mode, :transaction) do
:transaction ->
name = @reserved_prefix <> "COMMIT"
handle_transaction(name, :idle, :commit, opts, s)
:savepoint ->
name = @reserved_prefix <> "RELEASE SAVEPOINT postgrex_savepoint"
handle_savepoint([name], :release, opts, s)
end
end
@spec handle_rollback(Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_rollback(opts, s) do
case Keyword.get(opts, :mode, :transaction) do
:transaction ->
name = @reserved_prefix <> "ROLLBACK"
handle_transaction(name, :idle, :rollback, opts, s)
:savepoint ->
names = [@reserved_prefix <> "ROLLBACK TO SAVEPOINT postgrex_savepoint",
@reserved_prefix <> "RELEASE SAVEPOINT postgrex_savepoint"]
handle_savepoint(names, [:rollback, :release], opts, s)
end
end
@spec handle_simple(String.t, Keyword.t, state) ::
{:ok, Postgrex.Result.t, state} |
{:error | :disconnect, Postgrex.Error.t, state}
def handle_simple(statement, opts, %{buffer: buffer} = s) do
status = %{notify: notify(opts), sync: :sync}
simple_send(%{s | buffer: nil}, status, statement, buffer)
end
@spec handle_info(any, Keyword.t, state) ::
{:ok, state} | {:error | :disconnect, Postgrex.Error.t, state}
def handle_info(msg, opts \\ [], s)
def handle_info({:tcp, sock, data}, opts, %{sock: {:gen_tcp, sock}} = s) do
handle_data(s, opts, data)
end
def handle_info({:tcp_closed, sock}, _, %{sock: {:gen_tcp, sock}} = s) do
err = Postgrex.Error.exception(tag: :tcp, action: "async recv", reason: :closed)
{:disconnect, err, s}
end
def handle_info({:tcp_error, sock, reason}, _, %{sock: {:gen_tcp, sock}} = s) do
err = Postgrex.Error.exception(tag: :tcp, action: "async recv", reason: reason)
{:disconnect, err, s}
end
def handle_info({:ssl, sock, data}, opts, %{sock: {:ssl, sock}} = s) do
handle_data(s, opts, data)
end
def handle_info({:ssl_closed, sock}, _, %{sock: {:ssl, sock}} = s) do
err = Postgrex.Error.exception(tag: :ssl, action: "async recv", reason: :closed)
{:disconnect, err, s}
end
def handle_info({:ssl_error, sock, reason}, _, %{sock: {:ssl, sock}} = s) do
err = Postgrex.Error.exception(tag: :ssl, action: "async recv", reason: reason)
{:disconnect, err, s}
end
def handle_info(msg, _, s) do
Logger.info(fn() -> [inspect(__MODULE__), ?\s, inspect(self()),
" received unexpected message: " | inspect(msg)]
end)
{:ok, s}
end
## connect
defp connect(host, port, sock_opts, %{timeout: timeout} = s) do
buffer? = Keyword.has_key?(sock_opts, :buffer)
case :gen_tcp.connect(host, port, sock_opts ++ @sock_opts, timeout) do
{:ok, sock} when buffer? ->
{:ok, %{s | sock: {:gen_tcp, sock}}}
{:ok, sock} ->
# A suitable :buffer is only set if :recbuf is included in
# :socket_options.
{:ok, [sndbuf: sndbuf, recbuf: recbuf, buffer: buffer]} =
:inet.getopts(sock, [:sndbuf, :recbuf, :buffer])
buffer = buffer
|> max(sndbuf)
|> max(recbuf)
:ok = :inet.setopts(sock, [buffer: buffer])
{:ok, %{s | sock: {:gen_tcp, sock}}}
{:error, reason} ->
{:error, Postgrex.Error.exception(tag: :tcp, action: "connect", reason: reason)}
end
end
## ssl
defp ssl(s, status) do
case msg_send(s, msg_ssl_request(), "") do
:ok -> ssl_recv(s, status)
{:disconnect, _, _} = dis -> dis
end
end
defp ssl_recv(%{sock: {:gen_tcp, sock}, timeout: timeout} = s, status) do
case :gen_tcp.recv(sock, 1, timeout) do
{:ok, <<?S>>} ->
ssl_connect(s, status)
{:ok, <<?N>>} ->
disconnect(s, %Postgrex.Error{message: "ssl not available"}, "")
{:error, reason} ->
err = Postgrex.Error.exception(tag: :tcp, action: "recv", reason: reason)
disconnect(s, err, "")
end
end
defp ssl_connect(%{sock: {:gen_tcp, sock}, timeout: timeout} = s, status) do
case :ssl.connect(sock, status.opts[:ssl_opts] || [], timeout) do
{:ok, ssl_sock} ->
startup(%{s | sock: {:ssl, ssl_sock}}, status)
{:error, reason} ->
err = Postgrex.Error.exception(tag: :ssl, action: "connect", reason: reason)
disconnect(s, err, "")
end
end
## startup
defp startup(s, %{opts: opts} = status) do
params = opts[:parameters] || []
user = Keyword.fetch!(opts, :username)
database = Keyword.fetch!(opts, :database)
msg = msg_startup(params: [user: user, database: database] ++ params)
case msg_send(s, msg, "") do
:ok -> auth_recv(s, status, <<>>)
{:disconnect, _, _} = dis -> dis
end
end
## auth
defp auth_recv(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_auth(type: :ok), buffer} ->
init_recv(s, status, buffer)
{:ok, msg_auth(type: :cleartext), buffer} ->
auth_cleartext(s, status, buffer)
{:ok, msg_auth(type: :md5, data: salt), buffer} ->
auth_md5(s, status, salt, buffer)
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp auth_cleartext(s, %{opts: opts} = status, buffer) do
pass = Keyword.fetch!(opts, :password)
auth_send(s, msg_password(pass: pass), status, buffer)
end
defp auth_md5(s, %{opts: opts} = status, salt, buffer) do
user = Keyword.fetch!(opts, :username)
pass = Keyword.fetch!(opts, :password)
digest = :crypto.hash(:md5, [pass, user])
|> Base.encode16(case: :lower)
digest = :crypto.hash(:md5, [digest, salt])
|> Base.encode16(case: :lower)
auth_send(s, msg_password(pass: ["md5", digest]), status, buffer)
end
defp auth_send(s, msg, status, buffer) do
case msg_send(s, msg, buffer) do
:ok -> auth_recv(s, status, buffer)
{:disconnect, _, _} = dis -> dis
end
end
## init
defp init_recv(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_backend_key(pid: pid), buffer} ->
init_recv(%{s | connection_id: pid}, status, buffer)
{:ok, msg_ready(), buffer} ->
bootstrap(s, status, buffer)
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
init_recv(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## bootstrap
defp bootstrap(s, %{types_key: nil}, buffer) do
activate(s, buffer)
end
defp bootstrap(s, %{types_key: types_key} = status, buffer) do
case Postgrex.TypeServer.fetch(types_key) do
{:ok, table} ->
reserve_send(%{s | types: table}, status, buffer)
{:lock, ref, table} ->
status = %{status | types_ref: ref, types_table: table}
bootstrap_send(%{s | types: ref}, status, buffer)
end
end
defp bootstrap_send(%{parameters: parameters} = s, status, buffer) do
%{extensions: extensions} = status
extension_keys = Enum.map(extensions, &elem(&1, 0))
extension_opts = Types.prepare_extensions(extensions, parameters)
matchers = Types.extension_matchers(extension_keys, extension_opts)
version = parameters["server_version"] |> Postgrex.Utils.parse_version
statement = Types.bootstrap_query(matchers, version)
msg = msg_query(statement: statement)
case msg_send(s, msg, buffer) do
:ok ->
status = %{status | extension_info: {extension_keys, extension_opts}}
bootstrap_recv(s, status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp bootstrap_recv(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_row_desc(), buffer} ->
bootstrap_recv(s, status, [], buffer)
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
bootstrap_recv(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp bootstrap_recv(%{timeout: timeout} = s, status, rows, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_data_row(values: values), buffer} ->
bootstrap_recv(s, status, [row_decode(values) | rows], buffer)
{:ok, msg_command_complete(), buffer} ->
bootstrap_types(s, status, rows, buffer)
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
bootstrap_recv(handle_msg(s, status, msg), status, rows, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp bootstrap_types(s, %{types_table: table} = status, rows, buffer) do
%{types_ref: ref, extension_info: {extension_keys, extension_opts}} = status
types = Types.build_types(rows)
Types.associate_extensions_with_types(table, extension_keys, extension_opts, types)
Postgrex.TypeServer.unlock(ref)
bootstrap_sync_recv(%{s | types: table}, status, buffer)
end
defp bootstrap_sync_recv(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_ready(), buffer} ->
reserve_send(s, status, buffer)
{:ok, msg, buffer} ->
bootstrap_sync_recv(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp reserve_send(s, status, buffer) do
case msg_send(s, reserve_msgs() ++ [msg_sync()], buffer) do
:ok ->
reserve_recv(s, status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp reserve_msgs() do
for statement <- @reserved_queries do
name = @reserved_prefix <> statement
msg_parse(name: name, statement: statement, type_oids: [])
end
end
defp reserve_recv(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_parse_complete(), buffer} ->
reserve_recv(s, status, buffer)
{:ok, msg_ready(status: :idle), buffer} ->
activate(s, buffer)
{:ok, msg_ready(status: postgres), buffer} ->
sync_error(s, postgres, buffer)
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
reserve_recv(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## simple
defp simple_send(s, status, statement, buffer) do
msg = msg_query(statement: statement)
case msg_send(s, msg, buffer) do
:ok -> simple_recv(s, status, buffer)
{:disconnect, _, _} = dis -> dis
end
end
defp simple_recv(%{timeout: timeout} = s, status, buffer) do
## simple queries here are only done by Postgrex.Notifications processes
case msg_recv(s, timeout, buffer) do
{:ok, msg_command_complete(tag: tag), buffer} ->
complete(s, status, %Query{}, [], tag, buffer)
{:ok, msg_error(fields: fields), buffer} ->
err = Postgrex.Error.exception(postgres: fields)
sync_recv(s, status, nil, err, buffer)
{:ok, msg, buffer} ->
simple_recv(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## prepare
defp parse_describe_send(s, status, query, buffer) do
%Query{name: name, statement: statement} = query
msgs =
[msg_parse(name: name, statement: statement, type_oids: []),
msg_describe(type: :statement, name: name),
msg_flush()]
case msg_send(s, msgs, buffer) do
:ok ->
parse_recv(s, status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp describe_send(s, status, %Query{name: name} = query, buffer) do
msgs = [msg_describe(type: :statement, name: name), msg_flush()]
case msg_send(s, msgs, buffer) do
:ok ->
describe_recv(s, status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp parse_recv(s, %{prepare: prepare} = status, query, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_parse_complete(), buffer} when prepare == :parse_execute ->
query_put(s, query)
bind_recv(s, status, query, buffer)
{:ok, msg_parse_complete(), buffer} when prepare == :parse_describe ->
query_put(s, query)
describe_recv(s, status, query, buffer)
{:ok, msg_error(fields: fields), buffer} when prepare == :parse_execute ->
err = Postgrex.Error.exception(postgres: fields)
unnamed_query_delete(s, query)
sync_recv(s, status, query, err, buffer)
{:ok, msg_error(fields: fields), buffer} when prepare == :parse_describe ->
unnamed_query_delete(s, query)
sync(s, status, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
parse_recv(handle_msg(s, status, msg), status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp describe_recv(s, status, query, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_no_data(), buffer} ->
ok(s, %Query{query | types: s.types}, buffer)
{:ok, msg_parameter_desc(type_oids: param_oids), buffer} ->
describe_recv(s, status, %Query{query | encoders: param_oids}, buffer)
{:ok, msg_row_desc(fields: fields), buffer} ->
{col_oids, col_names} = columns(fields)
query = %Query{query | types: s.types, columns: col_names,
decoders: col_oids}
ok(s, query, buffer)
{:ok, msg_error(fields: fields), buffer} ->
sync(s, status, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
describe_recv(handle_msg(s, status, msg), status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## execute
defp handle_execute(query, params, sync, opts, s) do
%{types: types, buffer: buffer} = s
case query do
%Query{types: nil} ->
query_error(s, "query #{inspect query} has not been prepared")
%Query{types: ^types} = query ->
status = %{notify: notify(opts), sync: sync, prepare: :parse_execute}
execute_lookup(%{s | buffer: nil}, status, query, params, buffer)
%Query{} = query ->
query_error(s, "query #{inspect query} has invalid types for the connection")
end
end
defp query_error(s, msg) do
{:error, ArgumentError.exception(msg), s}
end
defp execute_lookup(s, status, query, params, buffer) do
case query_member?(s, query) do
true ->
execute_send(s, status, query, params, buffer)
false ->
parse_execute_send(s, status, query, params, buffer)
end
end
defp execute_send(s, status, query, params, buffer) do
%Query{param_formats: pfs, result_formats: rfs, name: name} = query
msgs = [
msg_bind(name_port: "", name_stat: name, param_formats: pfs, params: params, result_formats: rfs),
msg_execute(name_port: "", max_rows: 0) |
sync_msgs(status, name)]
case msg_send(s, msgs, buffer) do
:ok ->
bind_recv(s, status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp parse_execute_send(s, status, query, params, buffer) do
%Query{param_formats: pfs, result_formats: rfs, name: name, statement: statement} = query
msgs = [
msg_parse(name: name, statement: statement, type_oids: []),
msg_bind(name_port: "", name_stat: name, param_formats: pfs, params: params, result_formats: rfs),
msg_execute(name_port: "", max_rows: 0) |
sync_msgs(status, name)]
case msg_send(s, msgs, buffer) do
:ok ->
parse_recv(s, status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp sync_msgs(%{sync: :sync_close}, name) do
[msg_sync(), msg_close(type: :statement, name: name), msg_flush()]
end
defp sync_msgs(_, _) do
[msg_sync()]
end
defp bind_recv(s, status, query, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_bind_complete(), buffer} ->
execute_recv(s, status, query, buffer)
{:ok, msg_error(fields: fields), buffer} ->
bind_error(s, status, query, fields, buffer)
{:ok, msg, buffer} ->
bind_recv(handle_msg(s, status, msg), status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp bind_error(s, status, query, fields, buffer) do
err = Postgrex.Error.exception(postgres: fields)
_ = if err.postgres.code == :invalid_sql_statement_name do
Logger.error fn() ->
[inspect(query) | " is not prepared on backend"]
end
query_delete(s, query)
end
sync_recv(s, status, query, err, buffer)
end
defp execute_recv(s, status, query, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_data_row(values: values), buffer} ->
execute_recv(s, status, query, [values], buffer)
{:ok, msg_command_complete(tag: tag), buffer} ->
complete(s, status, query, [], tag, buffer)
{:ok, msg_empty_query(), buffer} ->
sync_recv(s, status, query, %Postgrex.Result{}, buffer)
{:ok, msg_error(fields: fields), buffer} ->
err = Postgrex.Error.exception(postgres: fields)
sync_recv(s, status, query, err, buffer)
{:ok, msg, buffer} ->
execute_recv(handle_msg(s, status, msg), status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp execute_recv(s, status, query, rows, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_data_row(values: values), buffer} ->
execute_recv(s, status, query, [values | rows], buffer)
{:ok, msg_command_complete(tag: tag), buffer} ->
complete(s, status, query, rows, tag, buffer)
{:ok, msg, buffer} ->
execute_recv(handle_msg(s, status, msg), status, query, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp complete(%{connection_id: connection_id} = s, status, query, rows, tag, buffer) do
{command, nrows} = decode_tag(tag)
%Query{columns: cols} = query
# Fix for PostgreSQL 8.4 (doesn't include number of selected rows in tag)
if is_nil(nrows) and command == :select do
nrows = length(rows)
end
if is_nil(cols) and rows == [] do
rows = nil
end
result = %Postgrex.Result{command: command, num_rows: nrows || 0,
rows: rows, columns: cols, connection_id: connection_id}
sync_recv(s, status, query, result, buffer)
end
## close
defp close(s, status, %Query{name: name} = query, result, buffer) do
msgs = [
msg_close(type: :statement, name: name),
msg_flush() ]
case msg_send(s, msgs, buffer) do
:ok ->
close_recv(s, status, query, result, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp close_recv(s, status, query, result, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_close_complete(), buffer} ->
query_delete(s, query)
ok(s, result, buffer)
{:ok, msg_error(fields: fields), buffer} ->
ok(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, buffer} ->
close_recv(handle_msg(s, status, msg), status, query, result, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## sync
defp sync(s, status, result, buffer) do
case msg_send(s, msg_sync(), buffer) do
:ok -> sync_recv(s, status, nil, result, buffer)
{:disconnect, _, _} = dis -> dis
end
end
## transaction
defp handle_transaction(name, postgres, cmd, opts, %{postgres: :naive} = s)
when postgres != :naive do
handle_transaction(name, :naive, cmd, opts, s)
end
defp handle_transaction(name, postgres, cmd, opts, s) do
%{connection_id: connection_id, buffer: buffer} = s
status = %{notify: notify(opts), sync: :sync}
res = %Postgrex.Result{command: cmd, connection_id: connection_id}
transaction_send(%{s | buffer: nil}, status, name, postgres, res, buffer)
end
defp transaction_send(s, status, name, postgres, res, buffer) do
msgs = transaction_msgs([name])
case msg_send(s, msgs, buffer) do
:ok ->
transaction_recv(s, status, postgres, res, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp transaction_msgs([]) do
[msg_sync()]
end
defp transaction_msgs([name | names]) do
[msg_bind(name_port: "", name_stat: name, param_formats: [], params: [], result_formats: []),
msg_execute(name_port: "" , max_rows: 0) |
transaction_msgs(names)]
end
defp transaction_recv(s, status, postgres, res, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_ready(), buffer} when postgres == :naive ->
ok(s, res, buffer)
{:ok, msg_ready(status: ^postgres), buffer} ->
ok(s, res, postgres, buffer)
{:ok, msg_ready(status: postgres), buffer} ->
sync_error(s, postgres, buffer)
{:ok, msg_bind_complete(), buffer} ->
transaction_recv(s, status, postgres, res, buffer)
{:ok, msg_command_complete(), buffer} ->
transaction_recv(s, status, postgres, res, buffer)
{:ok, msg_error(fields: fields), buffer} when postgres == :naive ->
err = Postgrex.Error.exception(postgres: fields)
sync_recv(s, status, nil, err, buffer)
{:ok, msg_error(fields: fields), buffer} ->
err = Postgrex.Error.exception(postgres: fields)
disconnect(s, err, buffer)
{:ok, msg, buffer} ->
s = handle_msg(s, status, msg)
transaction_recv(s, status, postgres, res, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp handle_savepoint(names, cmd, opts, s) do
%{connection_id: connection_id, buffer: buffer} = s
status = %{notify: notify(opts), sync: :sync}
res = %Postgrex.Result{command: cmd, connection_id: connection_id}
savepoint_send(%{s | buffer: nil}, status, names, res, buffer)
end
defp savepoint_send(s, status, names, res, buffer) do
msgs = transaction_msgs(names)
case msg_send(s, msgs, buffer) do
:ok ->
savepoint_recv(s, status, res, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp savepoint_recv(%{postgres: postgres} = s, status, res, buffer) do
case msg_recv(s, :infinity, buffer) do
{:ok, msg_bind_complete(), buffer} ->
savepoint_recv(s, status, res, buffer)
{:ok, msg_command_complete(), buffer} ->
savepoint_recv(s, status, res, buffer)
{:ok, msg_ready(status: :idle), buffer} when postgres == :transaction ->
sync_error(s, :idle, buffer)
{:ok, msg_ready(status: :transaction), buffer} when postgres == :idle ->
sync_error(s, :transaction, buffer)
{:ok, msg_ready(status: :failed), buffer} when postgres == :idle ->
sync_error(s, :failed, buffer)
{:ok, msg_ready(), buffer} ->
ok(s, res, buffer)
{:ok, msg_error(fields: fields), buffer} ->
err = Postgrex.Error.exception(postgres: fields)
sync_recv(s, status, nil, err, buffer)
{:ok, msg, buffer} ->
s = handle_msg(s, status, msg)
savepoint_recv(s, status, res, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## data
defp handle_data(s, opts, buffer) do
data(s, %{notify: notify(opts)}, buffer)
end
defp data(%{timeout: timeout} = s, status, buffer) do
case msg_recv(s, timeout, buffer) do
{:ok, msg_error(fields: fields), buffer} ->
disconnect(s, Postgrex.Error.exception(postgres: fields), buffer)
{:ok, msg, <<>>} ->
activate(handle_msg(s, status, msg), <<>>)
{:ok, msg, buffer} ->
data(handle_msg(s, status, msg), status, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
## helpers
defp notify(opts) do
opts[:notify] || fn(_, _) -> :ok end
end
defp columns(fields) do
Enum.map(fields, fn row_field(type_oid: oid, name: name) ->
{oid, name}
end) |> :lists.unzip
end
defp tag(:gen_tcp), do: :tcp
defp tag(:ssl), do: :ssl
defp decode_tag("INSERT " <> rest) do
[_oid, nrows] = :binary.split(rest, " ")
{:insert, String.to_integer(nrows)}
end
defp decode_tag("SELECT " <> int),
do: {:select, String.to_integer(int)}
defp decode_tag("UPDATE " <> int),
do: {:update, String.to_integer(int)}
defp decode_tag("DELETE " <> int),
do: {:delete, String.to_integer(int)}
defp decode_tag("FETCH " <> int),
do: {:fetch, String.to_integer(int)}
defp decode_tag("MOVE " <> int),
do: {:move, String.to_integer(int)}
defp decode_tag("COPY " <> int),
do: {:copy, String.to_integer(int)}
defp decode_tag("BEGIN"),
do: {:commit, nil}
defp decode_tag("COMMIT"),
do: {:commit, nil}
defp decode_tag("ROLLBACK"),
do: {:rollback, nil}
defp decode_tag(tag),
do: decode_tag(tag, "")
defp decode_tag(<<>>, acc),
do: {String.to_atom(acc), nil}
defp decode_tag(<<?\s, t::binary>>, acc),
do: decode_tag(t, <<acc::binary, ?_>>)
defp decode_tag(<<h, t::binary>>, acc) when h in ?A..?Z,
do: decode_tag(t, <<acc::binary, h+32>>)
defp decode_tag(<<h, t::binary>>, acc),
do: decode_tag(t, <<acc::binary, h>>)
# It is ok to use infinity timeout here if in client process as timer is
# running.
defp msg_recv(%{sock: {:gen_tcp, sock}} = s, timeout, :active_once) do
receive do
{:tcp, ^sock, buffer} ->
msg_recv(s, timeout, buffer)
{:tcp_closed, ^sock} ->
disconnect(s, :tcp, "async_recv", :closed, :active_once)
{:tcp_error, ^sock, reason} ->
disconnect(s, :tcp, "async_recv", reason, :active_once)
after
timeout ->
disconnect(s, :tcp, "async_recv", :timeout, :active_one)
end
end
defp msg_recv(%{sock: {:ssl, sock}} = s, timeout, :active_once) do
receive do
{:ssl, ^sock, buffer} ->
msg_recv(s, timeout, buffer)
{:ssl_closed, ^sock} ->
disconnect(s, :ssl, "async_recv", :closed, :active_once)
{:ssl_error, ^sock, reason} ->
disconnect(s, :ssl, "async_recv", reason, :active_once)
after
timeout ->
disconnect(s, :ssl, "async_recv", :timeout, :active_once)
end
end
defp msg_recv(s, timeout, buffer) do
case msg_decode(buffer) do
{:ok, _, _} = ok -> ok
{:more, more} -> msg_recv(s, timeout, buffer, more)
end
end
defp msg_recv(%{sock: {mod, sock}} = s, timeout, buffer, more) do
case mod.recv(sock, more, timeout) do
{:ok, data} ->
msg_recv(s, timeout, buffer <> data)
{:error, reason} ->
disconnect(s, tag(mod), "recv", reason, buffer)
end
end
defp msg_decode(bin) when byte_size(bin) < 5 do
{:more, 0}
end
defp msg_decode(<<type :: int8, size :: int32, rest :: binary>>) do
size = size - 4
case rest do
<<body :: binary(size), rest :: binary>> ->
{:ok, parse(body, type, size), rest}
_ ->
{:more, size - byte_size(rest)}
end
end
defp row_decode(<<>>), do: []
defp row_decode(<<-1::int32, rest::binary>>) do
[nil | row_decode(rest)]
end
defp row_decode(<<len::uint32, value::binary(len), rest::binary>>) do
[value | row_decode(rest)]
end
defp msg_send(s, msgs, buffer) when is_list(msgs) do
binaries = Enum.reduce(msgs, [], &[&2 | encode_msg(&1)])
do_send(s, binaries, buffer)
end
defp msg_send(s, msg, buffer) do
do_send(s, encode_msg(msg), buffer)
end
defp do_send(%{sock: {mod, sock}} = s, data, buffer) do
case mod.send(sock, data) do
:ok ->
:ok
{:error, reason} ->
disconnect(s, tag(mod), "send", reason, buffer)
end
end
defp handle_msg(s, _, msg_parameter(name: name, value: value)) do
%{parameters: parameters} = s
# Binaries likely part of much larger binary and only keeping name/value
# over long term
name = :binary.copy(name)
value = :binary.copy(value)
cond do
is_reference(parameters) ->
_ = Postgrex.Parameters.put(parameters, name, value)
s
is_map(parameters) ->
%{s | parameters: Map.put(parameters, name, value)}
end
end
defp handle_msg(s, status, msg_notify(channel: channel, payload: payload)) do
%{notify: notify} = status
notify.(channel, payload)
s
end
defp handle_msg(s, _, msg_notice()) do
# TODO: subscribers
s
end
defp ok(s, %Postgrex.Result{} = res, buffer) do
{:ok, res, %{s | buffer: buffer}}
end
defp ok(s, %Postgrex.Query{} = query, buffer) do
{:ok, query, %{s | buffer: buffer}}
end
defp ok(%{connection_id: connection_id} = s, %Postgrex.Error{} = err, buffer) do
{:error, %{err | connection_id: connection_id}, %{s | buffer: buffer}}
end
defp ok(s, :active_once, buffer) do
activate(s, buffer)
end
defp ok(s, nil, buffer) do
{:ok, %{s | buffer: buffer}}
end
defp ok(s, %Postgrex.Result{} = res, postgres, buffer)
when postgres in [:idle, :transaction] do
{:ok, res, %{s | postgres: postgres, buffer: buffer}}
end
defp disconnect(s, tag, action, reason, buffer) do
err = Postgrex.Error.exception(tag: tag, action: action, reason: reason)
disconnect(s, err, buffer)
end
defp disconnect(%{connection_id: connection_id} = s, %Postgrex.Error{} = err, buffer) do
{:disconnect, %{err | connection_id: connection_id}, %{s | buffer: buffer}}
end
defp reserved_error(query, s) do
err = ArgumentError.exception("query #{inspect query} uses reserved name")
{:error, err, s}
end
# Query has completed so ok to use state timeout as message should either be
# buffer or in flight. sync_recv/5 used by simple queries so can't use
# :infinity.
defp sync_recv(s, %{sync: sync} = status, query, result, buffer) do
%{postgres: postgres, timeout: timeout} = s
case msg_recv(s, timeout, buffer) do
{:ok, msg_ready(status: :idle), buffer} when postgres == :transaction ->
sync_error(s, :idle, buffer)
{:ok, msg_ready(status: :transaction), buffer} when postgres == :idle ->
sync_error(s, :transaction, buffer)
{:ok, msg_ready(status: :failed), buffer} when postgres == :idle ->
sync_error(s, :failed, buffer)
{:ok, msg_ready(), buffer} when sync == :sync ->
ok(s, result, buffer)
{:ok, msg_ready(), buffer} when sync == :sync_close ->
close_recv(s, status, query, result, buffer)
{:ok, msg, buffer} ->
sync_recv(handle_msg(s, status, msg), status, query, result, buffer)
{:disconnect, _, _} = dis ->
dis
end
end
defp sync_error(s, postgres, buffer) do
sync_error(%{s | buffer: buffer}, postgres)
end
defp sync_error(s, postgres) do
err = %Postgrex.Error{message: "unexpected postgres status: #{postgres}"}
{:disconnect, err, s}
end
defp recv_buffer(%{sock: {:gen_tcp, sock}} = s) do
receive do
{:tcp, ^sock, buffer} ->
{:ok, %{s | buffer: buffer}}
{:tcp_closed, ^sock} ->
disconnect(s, :tcp, "async recv", :closed, "")
{:tcp_error, ^sock, reason} ->
disconnect(s, :tcp, "async_recv", reason, "")
after
0 ->
{:ok, %{s | buffer: <<>>}}
end
end
defp recv_buffer(%{sock: {:ssl, sock}} = s) do
receive do
{:ssl, ^sock, buffer} ->
{:ok, %{s | buffer: buffer}}
{:ssl_closed, ^sock} ->
disconnect(s, :ssl, "async recv", :closed, "")
{:ssl_error, ^sock, reason} ->
disconnect(s, :ssl, "async recv", reason, "")
after
0 ->
{:ok, %{s | buffer: <<>>}}
end
end
## Fake [active: once] if buffer not empty
defp activate(s, <<>>) do
case setopts(s, [active: :once], <<>>) do
:ok -> {:ok, %{s | buffer: :active_once}}
other -> other
end
end
defp activate(%{sock: {mod, sock}} = s, buffer) do
_ = send(self(), {tag(mod), sock, buffer})
{:ok, s}
end
defp setopts(%{sock: {mod, sock}} = s, opts, buffer) do
case setopts(mod, sock, opts) do
:ok ->
:ok
{:error, reason} ->
disconnect(s, tag(mod), "setopts", reason, buffer)
end
end
defp setopts(:gen_tcp, sock, opts), do: :inet.setopts(sock, opts)
defp setopts(:ssl, sock, opts), do: :ssl.setopts(sock, opts)
defp sock_close(%{sock: {mod, sock}}), do: mod.close(sock)
defp delete_parameters(%{parameters: ref}) when is_reference(ref) do
Postgrex.Parameters.delete(ref)
end
defp delete_parameters(_), do: :ok
defp queries_new(), do: :ets.new(__MODULE__, [:set, :public])
defp queries_delete(%{queries: nil}), do: true
defp queries_delete(%{queries: queries}), do: :ets.delete(queries)
defp query_put(%{queries: queries}, query) do
%Query{name: name, statement: statement} = query
try do
:ets.insert(queries, {name, statement})
rescue
ArgumentError ->
# ets table deleted, socket will be closed, rescue here and get nice
# error when trying to recv on socket.
:ok
else
true ->
:ok
end
end
defp unnamed_query_delete(s, %Query{name: ""} = query) do
query_delete(s, query)
end
defp unnamed_query_delete(_, _), do: :ok
defp query_delete(%{queries: queries}, %Query{name: name}) do
try do
:ets.delete(queries, name)
rescue
ArgumentError ->
:ok
else
true ->
:ok
end
end
defp query_member?(%{queries: queries}, query) do
%Query{name: name, statement: statement} = query
try do
:ets.lookup(queries, name)
rescue
ArgumentError ->
false
else
[{_, ^statement}] ->
true
_ ->
false
end
end
end