Current section

18 Versions

Jump to

Compare versions

13 files changed
+256 additions
-177 deletions
  @@ -57,52 +57,60 @@ see `CHANGES` for full list.
57 57 (thousands of messages).
58 58
59 59 ## Usage
60 +
60 61 ### Connect
61 62
62 63 ```erlang
63 - -type host() :: inet:ip_address() | inet:hostname().
64 + connect(Opts) -> {ok, Connection :: epgsql:connection()} | {error, Reason :: epgsql:connect_error()}
65 + when
66 + Opts ::
67 + #{host := inet:ip_address() | inet:hostname(),
68 + username := iodata(),
69 + password => iodata() | fun( () -> iodata() ),
70 + database => iodata(),
71 + port => inet:port_number(),
72 + ssl => boolean() | required,
73 + ssl_opts => [ssl:ssl_option()], % @see OTP ssl app, ssl_api.hrl
74 + timeout => timeout(), % socket connect timeout, default: 5000 ms
75 + async => pid() | atom(), % process to receive LISTEN/NOTIFY msgs
76 + codecs => [{epgsql_codec:codec_mod(), any()}]}
77 + replication => Replication :: string()} % Pass "database" to connect in replication mode
78 + | list().
64 79
65 - -type connect_option() ::
66 - {database, DBName :: string()} |
67 - {port, PortNum :: inet:port_number()} |
68 - {ssl, IsEnabled :: boolean() | required} |
69 - {ssl_opts, SslOptions :: [ssl:ssl_option()]} | % @see OTP ssl app, ssl_api.hrl
70 - {timeout, TimeoutMs :: timeout()} | % default: 5000 ms
71 - {async, Receiver :: pid() | atom()} | % process to receive LISTEN/NOTIFY msgs
72 - {codecs, Codecs :: [{epgsql_codec:codec_mod(), any()}]} |
73 - {replication, Replication :: string()}. % Pass "database" to connect in replication mode
74 -
75 - -spec connect(host(), string(), string(), [connect_option()] | map())
76 - -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
77 - %% @doc connects to Postgres
78 - %% where
79 - %% `Host' - host to connect to
80 - %% `Username' - username to connect as, defaults to `$USER'
81 - %% `Password' - optional password to authenticate with
82 - %% `Opts' - proplist or map of extra options
83 - %% returns `{ok, Connection}' otherwise `{error, Reason}'
84 - connect(Host, Username, Password, Opts) -> ...
80 + connect(Host, Username, Password, Opts) -> {ok, C} | {error, Reason}.
85 81 ```
86 82 example:
87 83 ```erlang
88 - {ok, C} = epgsql:connect("localhost", "username", "psss", [
89 - {database, "test_db"},
90 - {timeout, 4000}
91 - ]),
84 + {ok, C} = epgsql:connect("localhost", "username", "psss", #{
85 + database => "test_db",
86 + timeout => 4000
87 + }),
92 88 ...
93 89 ok = epgsql:close(C).
94 90 ```
95 91
96 - The `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
97 - socket fails to connect within `TimeoutMs` milliseconds.
92 + Only `host` and `username` are mandatory, but most likely you would need `database` and `password`.
98 93
99 - Options may be passed as map with the same key names, if your VM version supports maps.
94 + - `password` - DB user password. It might be provided as string / binary or as a fun that returns
95 + string / binary. Internally, plain password is wrapped to anonymous fun before it is sent to connection
96 + process, so, if `connect` command crashes, plain password will not appear in crash logs.
97 + - `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
98 + socket fails to connect within `TimeoutMs` milliseconds.
99 + - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted
100 + if encryption isn't supported by server. if set to `required` connection will fail if encryption
101 + is not available.
102 + - `ssl_opts` will be passed as is to `ssl:connect/3`
103 + - `async` see [Server notifications](#server-notifications)
104 + - `codecs` see [Pluggable datatype codecs](#pluggable-datatype-codecs)
105 + - `replication` see [Streaming replication protocol](#streaming-replication-protocol)
106 +
107 + Options may be passed as proplist or as map with the same key names.
100 108
101 109 Asynchronous connect example (applies to **epgsqli** too):
102 110
103 111 ```erlang
104 112 {ok, C} = epgsqla:start_link(),
105 - Ref = epgsqla:connect(C, "localhost", "username", "psss", [{database, "test_db"}]),
113 + Ref = epgsqla:connect(C, "localhost", "username", "psss", #{database => "test_db"}),
106 114 receive
107 115 {C, Ref, connected} ->
108 116 {ok, C};
  @@ -221,7 +229,7 @@ receive
221 229 end.
222 230 ```
223 231
224 - ## Extended Query
232 + ### Extended Query
225 233
226 234 ```erlang
227 235 {ok, Columns, Rows} = epgsql:equery(C, "select ...", [Parameters]).
  @@ -267,17 +275,17 @@ end.
267 275 `epgsqli:equery(C, Statement, [TypedParameters])` sends same set of messages as
268 276 squery including final `{C, Ref, done}`.
269 277
270 - ## Prepared Query
278 + ### Prepared Query
271 279 ```erlang
272 280 {ok, Columns, Rows} = epgsql:prepared_query(C, StatementName, [Parameters]).
273 281 {ok, Count} = epgsql:prepared_query(C, StatementName, [Parameters]).
274 282 {ok, Count, Columns, Rows} = epgsql:prepared_query(C, StatementName, [Parameters]).
275 - {error, Error} = epgsql:prepared_equery(C, "non_existent_query", [Parameters]).
283 + {error, Error} = epgsql:prepared_query(C, "non_existent_query", [Parameters]).
276 284 ```
277 285 `Parameters` - optional list of values to be bound to `$1`, `$2`, `$3`, etc.
278 286 `StatementName` - name of query given with ```erlang epgsql:parse(C, StatementName, "select ...", []).```
279 287
280 - With prepared query one can parse a query giving it a name with `epgsql:parse` on start and reuse the name
288 + With prepared query one can parse a query giving it a name with `epgsql:parse` on start and reuse the name
281 289 for all further queries with different parameters.
282 290 ```erlang
283 291 epgsql:parse(C, "inc", "select $1+1", []).
  @@ -302,7 +310,7 @@ end.
302 310 `epgsqli:prepared_query(C, Statement, [TypedParameters])` sends same set of messages as
303 311 squery including final `{C, Ref, done}`.
304 312
305 - ## Parse/Bind/Execute
313 + ### Parse/Bind/Execute
306 314
307 315 ```erlang
308 316 {ok, Statement} = epgsql:parse(C, [StatementName], Sql, [ParameterTypes]).
  @@ -360,7 +368,7 @@ All epgsql functions return `{error, Error}` when an error occurs.
360 368 `epgsqla`/`epgsqli` modules' `close` and `sync` functions send `{C, Ref, ok}`.
361 369
362 370
363 - ## Batch execution
371 + ### Batch execution
364 372
365 373 Batch execution is `bind` + `execute` for several prepared statements.
366 374 It uses unnamed portals and `MaxRows = 0`.
  @@ -420,7 +428,9 @@ PG type | Representation
420 428 cidr | `{ip_address(), Mask :: 0..32}`
421 429 macaddr(8) | tuple of 6 or 8 `byte()`
422 430 geometry | `ewkb:geometry()`
423 -
431 + tsrange | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
432 + tstzrange | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
433 + daterange | `{{Year, Month, Day}, {Year, Month, Day}}`
424 434
425 435 `timestamp` and `timestamptz` parameters can take `erlang:now()` format: `{MegaSeconds, Seconds, MicroSeconds}`
426 436
  @@ -428,6 +438,9 @@ PG type | Representation
428 438 bracket and parentheses respectively. Additionally, infinities are represented by the atoms `minus_infinity`
429 439 and `plus_infinity`
430 440
441 + `tsrange`, `tstzrange`, `daterange` are range types for `timestamp`, `timestamptz` and `date`
442 + respectively. They can return `empty` atom as the result from a database if bounds are equal
443 +
431 444 ## Errors
432 445
433 446 Errors originating from the PostgreSQL backend are returned as `{error, #error{}}`,
  @@ -1,5 +1,5 @@
1 1 {<<"name">>,<<"epgsql">>}.
2 - {<<"version">>,<<"4.1.0">>}.
2 + {<<"version">>,<<"4.2.0">>}.
3 3 {<<"requirements">>,#{}}.
4 4 {<<"app">>,<<"epgsql">>}.
5 5 {<<"precompiled">>,false}.
  @@ -7,8 +7,8 @@
7 7 {<<"files">>,
8 8 [<<"src/epgsql.app.src">>,<<"LICENSE">>,<<"README.md">>,
9 9 <<"include/epgsql.hrl">>,<<"include/epgsql_geometry.hrl">>,
10 - <<"include/protocol.hrl">>,<<"rebar.config">>,<<"rebar.config.script">>,
11 - <<"rebar.lock">>,<<"src/commands/epgsql_cmd_batch.erl">>,
10 + <<"include/protocol.hrl">>,<<"rebar.config">>,<<"rebar.lock">>,
11 + <<"src/commands/epgsql_cmd_batch.erl">>,
12 12 <<"src/commands/epgsql_cmd_bind.erl">>,
13 13 <<"src/commands/epgsql_cmd_close.erl">>,
14 14 <<"src/commands/epgsql_cmd_connect.erl">>,
  @@ -35,6 +35,7 @@
35 35 <<"src/datatypes/epgsql_codec_noop.erl">>,
36 36 <<"src/datatypes/epgsql_codec_postgis.erl">>,
37 37 <<"src/datatypes/epgsql_codec_text.erl">>,
38 + <<"src/datatypes/epgsql_codec_timerange.erl">>,
38 39 <<"src/datatypes/epgsql_codec_uuid.erl">>,<<"src/epgsql.erl">>,
39 40 <<"src/epgsql_binary.erl">>,<<"src/epgsql_codec.erl">>,
40 41 <<"src/epgsql_command.erl">>,<<"src/epgsql_errcodes.erl">>,
  @@ -1,6 +1,4 @@
1 - {erl_opts, [{platform_define, "^[0-9]+", have_maps},
2 - {platform_define, "^(1[89])|^([2-9][0-9])", 'FAST_MAPS'}, % Erlang >=18
3 - {platform_define, "^(R|1|20)", 'FUN_STACKTRACE'}]}. % Erlang < 21
1 + {erl_opts, [{platform_define, "^17", 'SLOW_MAPS'}]}. % Erlang 17
4 2
5 3 {eunit_opts, [verbose]}.
6 4
  @@ -22,16 +20,15 @@
22 20 {ct_hooks, [epgsql_cth]}
23 21 ]}.
24 22
25 - %% See rebar.config.script
26 - %% {elvis,
27 - %% [#{dirs => ["src", "src/*"],
28 - %% include_dirs => ["include"],
29 - %% filter => "*.erl",
30 - %% ruleset => erl_files,
31 - %% rules =>
32 - %% [{elvis_style, line_length, #{limit => 120}},
33 - %% {elvis_style, god_modules, #{limit => 40}},
34 - %% {elvis_style, state_record_and_type, disable} % epgsql_sock
35 - %% ]}
36 - %% ]
37 - %% }.
23 + {elvis,
24 + [#{dirs => ["src", "src/*"],
25 + include_dirs => ["include"],
26 + filter => "*.erl",
27 + ruleset => erl_files,
28 + rules =>
29 + [{elvis_style, line_length, #{limit => 120}},
30 + {elvis_style, god_modules, #{limit => 40}},
31 + {elvis_style, state_record_and_type, disable} % epgsql_sock
32 + ]}
33 + ]
34 + }.
  @@ -1,19 +0,0 @@
1 - %% Elvis config uses maps, but they are not available in Erlang < 17
2 - case erlang:is_builtin(erlang, is_map, 1) of
3 - true ->
4 - [{elvis,
5 - [maps:from_list(
6 - [{dirs, ["src", "src/*"]},
7 - {include_dirs, ["include"]},
8 - {filter, "*.erl"},
9 - {ruleset, erl_files},
10 - {rules,
11 - [{elvis_style, line_length, maps:from_list([{limit, 120}])},
12 - {elvis_style, god_modules, maps:from_list([{limit, 40}])},
13 - {elvis_style, state_record_and_type, disable} % epgsql_sock
14 - ]}
15 - ])
16 - ]
17 - } | CONFIG];
18 - false -> CONFIG
19 - end.
  @@ -4,6 +4,7 @@
4 4 %%%
5 5 -module(epgsql_cmd_connect).
6 6 -behaviour(epgsql_command).
7 + -export([hide_password/1, opts_hide_password/1]).
7 8 -export([init/1, execute/2, handle_message/4]).
8 9 -export_type([response/0, connect_error/0]).
9 10
  @@ -27,7 +28,7 @@
27 28 | unknown).
28 29
29 30 -record(connect,
30 - {opts :: list(),
31 + {opts :: map(),
31 32 auth_fun :: auth_fun() | undefined,
32 33 auth_state :: any() | undefined,
33 34 auth_send :: {integer(), iodata()} | undefined,
  @@ -41,19 +42,14 @@
41 42 -define(AUTH_SASL_CONTINUE, 11).
42 43 -define(AUTH_SASL_FINAL, 12).
43 44
44 - init({Host, Username, Password, Opts}) ->
45 - Opts1 = [{host, Host},
46 - {username, Username},
47 - {password, Password}
48 - | Opts],
49 - #connect{opts = Opts1}.
45 + init(#{host := _, username := _} = Opts) ->
46 + #connect{opts = Opts}.
50 47
51 48 execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
52 - Host = get_val(host, Opts),
53 - Username = get_val(username, Opts),
54 - %% _ = get_val(password, Opts),
55 - Timeout = proplists:get_value(timeout, Opts, 5000),
56 - Port = proplists:get_value(port, Opts, 5432),
49 + #{host := Host,
50 + username := Username} = Opts,
51 + Timeout = maps:get(timeout, Opts, 5000),
52 + Port = maps:get(port, Opts, 5432),
57 53 SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}, {keepalive, true}],
58 54 case gen_tcp:connect(Host, Port, SockOpts, Timeout) of
59 55 {ok, Sock} ->
  @@ -67,29 +63,27 @@ execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
67 63 inet:getopts(Sock, [recbuf, sndbuf]),
68 64 inet:setopts(Sock, [{buffer, max(RecBufSize, SndBufSize)}]),
69 65
70 - PgSock1 = maybe_ssl(Sock, proplists:get_value(ssl, Opts, false), Opts, PgSock),
66 + PgSock1 = maybe_ssl(Sock, maps:get(ssl, Opts, false), Opts, PgSock),
71 67
72 68 Opts2 = ["user", 0, Username, 0],
73 - Opts3 = case proplists:get_value(database, Opts, undefined) of
74 - undefined -> Opts2;
75 - Database -> [Opts2 | ["database", 0, Database, 0]]
69 + Opts3 = case maps:find(database, Opts) of
70 + error -> Opts2;
71 + {ok, Database} -> [Opts2 | ["database", 0, Database, 0]]
76 72 end,
77 73
78 - Replication = proplists:get_value(replication, Opts, undefined),
79 - Opts4 = case Replication of
80 - undefined -> Opts3;
81 - Replication ->
82 - [Opts3 | ["replication", 0, Replication, 0]]
74 + {Opts4, PgSock2} =
75 + case Opts of
76 + #{replication := Replication} ->
77 + {[Opts3 | ["replication", 0, Replication, 0]],
78 + epgsql_sock:init_replication_state(PgSock1)};
79 + _ -> {Opts3, PgSock1}
83 80 end,
84 - PgSock2 = case Replication of
85 - undefined -> PgSock1;
86 - _ -> epgsql_sock:init_replication_state(PgSock1)
87 - end,
88 81
89 82 epgsql_sock:send(PgSock2, [<<196608:?int32>>, Opts4, 0]),
90 - PgSock3 = case proplists:get_value(async, Opts, undefined) of
91 - undefined -> PgSock2;
92 - Async -> epgsql_sock:set_attr(async, Async, PgSock2)
83 + PgSock3 = case Opts of
84 + #{async := Async} ->
85 + epgsql_sock:set_attr(async, Async, PgSock2);
86 + _ -> PgSock2
93 87 end,
94 88 {ok, PgSock3, State#connect{stage = maybe_auth}};
95 89 {error, Reason} = Error ->
  @@ -100,15 +94,35 @@ execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
100 94 {ok, PgSock, St#connect{auth_send = undefined}}.
101 95
102 96
97 + %% @doc Replace `password' in Opts map with obfuscated one
98 + opts_hide_password(#{password := Password} = Opts) ->
99 + HiddenPassword = hide_password(Password),
100 + Opts#{password => HiddenPassword};
101 + opts_hide_password(Opts) -> Opts.
102 +
103 +
104 + %% @doc this function wraps plaintext password to a lambda function, so, if
105 + %% epgsql_sock process crashes when executing `connect` command, password will
106 + %% not appear in a crash log
107 + -spec hide_password(iodata()) -> fun( () -> iodata() ).
108 + hide_password(Password) when is_list(Password);
109 + is_binary(Password) ->
110 + fun() ->
111 + Password
112 + end;
113 + hide_password(PasswordFun) when is_function(PasswordFun, 0) ->
114 + PasswordFun.
115 +
116 +
103 117 maybe_ssl(S, false, _, PgSock) ->
104 118 epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
105 119 maybe_ssl(S, Flag, Opts, PgSock) ->
106 120 ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>),
107 - Timeout = proplists:get_value(timeout, Opts, 5000),
121 + Timeout = maps:get(timeout, Opts, 5000),
108 122 {ok, <<Code>>} = gen_tcp:recv(S, 1, Timeout),
109 123 case Code of
110 124 $S ->
111 - SslOpts = proplists:get_value(ssl_opts, Opts, []),
125 + SslOpts = maps:get(ssl_opts, Opts, []),
112 126 case ssl:connect(S, SslOpts, Timeout) of
113 127 {ok, S2} ->
114 128 epgsql_sock:set_net_socket(ssl, S2, PgSock);
  @@ -167,14 +181,14 @@ auth_handle(Data, PgSock, #connect{auth_fun = Fun, auth_state = AuthSt} = St) ->
167 181
168 182 %% AuthenticationCleartextPassword
169 183 auth_cleartext(init, _AuthState, #connect{opts = Opts}) ->
170 - Password = get_val(password, Opts),
184 + Password = get_password(Opts),
171 185 {send, ?PASSWORD, [Password, 0], undefined};
172 186 auth_cleartext(_, _, _) -> unknown.
173 187
174 188 %% AuthenticationMD5Password
175 189 auth_md5(init, Salt, #connect{opts = Opts}) ->
176 - User = get_val(username, Opts),
177 - Password = get_val(password, Opts),
190 + User = maps:get(username, Opts),
191 + Password = get_password(Opts),
178 192 Digest1 = hex(erlang:md5([Password, User])),
179 193 Str = ["md5", hex(erlang:md5([Digest1, Salt])), 0],
180 194 {send, ?PASSWORD, Str, undefined};
  @@ -182,14 +196,14 @@ auth_md5(_, _, _) -> unknown.
182 196
183 197 %% AuthenticationSASL
184 198 auth_scram(init, undefined, #connect{opts = Opts}) ->
185 - User = get_val(username, Opts),
199 + User = maps:get(username, Opts),
186 200 Nonce = epgsql_scram:get_nonce(16),
187 201 ClientFirst = epgsql_scram:get_client_first(User, Nonce),
188 202 SaslInitialResponse = [?SCRAM_AUTH_METHOD, 0, <<(iolist_size(ClientFirst)):?int32>>, ClientFirst],
189 203 {send, ?SASL_ANY_RESPONSE, SaslInitialResponse, {auth_request, Nonce}};
190 204 auth_scram(<<?AUTH_SASL_CONTINUE:?int32, ServerFirst/binary>>, {auth_request, Nonce}, #connect{opts = Opts}) ->
191 - User = get_val(username, Opts),
192 - Password = get_val(password, Opts),
205 + User = maps:get(username, Opts),
206 + Password = get_password(Opts),
193 207 ServerFirstParts = epgsql_scram:parse_server_first(ServerFirst, Nonce),
194 208 {ClientFinalMessage, ServerProof} = epgsql_scram:get_client_final(ServerFirstParts, Nonce, User, Password),
195 209 {send, ?SASL_ANY_RESPONSE, ClientFinalMessage, {server_final, ServerProof}};
  @@ -243,10 +257,10 @@ handle_message(_, _, _, _) ->
243 257 unknown.
244 258
245 259
246 - get_val(Key, Proplist) ->
247 - Val = proplists:get_value(Key, Proplist),
248 - (Val =/= undefined) orelse error({required_option, Key}),
249 - Val.
260 + get_password(Opts) ->
261 + PasswordFun = maps:get(password, Opts),
262 + PasswordFun().
263 +
250 264
251 265 hex(Bin) ->
252 266 HChar = fun(N) when N < 10 -> $0 + N;
Loading more files…