Browse Source

Merge branch 'devel'

Sergey Prokhorov 5 years ago
parent
commit
563476a6ed
58 changed files with 1203 additions and 351 deletions
  1. 5 0
      .gitignore
  2. 3 2
      .travis.yml
  3. 39 0
      CHANGES
  4. 20 3
      Makefile
  5. 115 39
      README.md
  6. 57 0
      doc/overview.edoc
  7. 0 0
      doc/pluggable_commands.md
  8. 0 0
      doc/pluggable_types.md
  9. 0 0
      doc/streaming.md
  10. 3 3
      generate_errcodes_src.sh
  11. 21 3
      include/epgsql.hrl
  12. 1 0
      include/protocol.hrl
  13. 4 4
      rebar.config
  14. 74 23
      src/commands/epgsql_cmd_batch.erl
  15. 6 0
      src/commands/epgsql_cmd_bind.erl
  16. 4 0
      src/commands/epgsql_cmd_close.erl
  17. 30 19
      src/commands/epgsql_cmd_connect.erl
  18. 9 5
      src/commands/epgsql_cmd_describe_portal.erl
  19. 7 2
      src/commands/epgsql_cmd_describe_statement.erl
  20. 9 0
      src/commands/epgsql_cmd_equery.erl
  21. 8 0
      src/commands/epgsql_cmd_execute.erl
  22. 4 0
      src/commands/epgsql_cmd_parse.erl
  23. 5 1
      src/commands/epgsql_cmd_prepared_query.erl
  24. 7 0
      src/commands/epgsql_cmd_squery.erl
  25. 5 0
      src/commands/epgsql_cmd_start_replication.erl
  26. 5 0
      src/commands/epgsql_cmd_sync.erl
  27. 1 1
      src/commands/epgsql_cmd_update_type_cache.erl
  28. 5 2
      src/datatypes/epgsql_codec_boolean.erl
  29. 12 5
      src/datatypes/epgsql_codec_bpchar.erl
  30. 11 4
      src/datatypes/epgsql_codec_datetime.erl
  31. 5 2
      src/datatypes/epgsql_codec_float.erl
  32. 9 3
      src/datatypes/epgsql_codec_geometric.erl
  33. 65 32
      src/datatypes/epgsql_codec_hstore.erl
  34. 29 4
      src/datatypes/epgsql_codec_integer.erl
  35. 20 3
      src/datatypes/epgsql_codec_intrange.erl
  36. 8 3
      src/datatypes/epgsql_codec_json.erl
  37. 6 3
      src/datatypes/epgsql_codec_net.erl
  38. 1 0
      src/datatypes/epgsql_codec_noop.erl
  39. 9 2
      src/datatypes/epgsql_codec_postgis.erl
  40. 13 4
      src/datatypes/epgsql_codec_text.erl
  41. 30 6
      src/datatypes/epgsql_codec_timerange.erl
  42. 6 3
      src/datatypes/epgsql_codec_uuid.erl
  43. 1 1
      src/epgsql.app.src
  44. 82 49
      src/epgsql.erl
  45. 118 51
      src/epgsql_binary.erl
  46. 3 0
      src/epgsql_codec.erl
  47. 4 2
      src/epgsql_command.erl
  48. 19 2
      src/epgsql_errcodes.erl
  49. 3 3
      src/epgsql_oid_db.erl
  50. 7 5
      src/epgsql_scram.erl
  51. 30 13
      src/epgsql_sock.erl
  52. 27 15
      src/epgsql_wire.erl
  53. 12 1
      src/epgsqla.erl
  54. 12 1
      src/epgsqli.erl
  55. 7 2
      src/ewkb.erl
  56. 168 20
      test/epgsql_SUITE.erl
  57. 12 1
      test/epgsql_cast.erl
  58. 27 4
      test/epgsql_incremental.erl

+ 5 - 0
.gitignore

@@ -1,3 +1,8 @@
 _build
 _build
 rebar3
 rebar3
 datadir/
 datadir/
+doc/*
+!doc/overview.edoc
+!doc/*.md
+*.idea/*
+*.iml

+ 3 - 2
.travis.yml

@@ -11,13 +11,14 @@ install: "true"
 language: erlang
 language: erlang
 matrix:
 matrix:
   include:
   include:
-    - otp_release: 22.0
+    - otp_release: 22.2
     - otp_release: 21.3
     - otp_release: 21.3
     - otp_release: 20.3
     - otp_release: 20.3
     - otp_release: 19.3
     - otp_release: 19.3
     - otp_release: 18.3
     - otp_release: 18.3
       dist: trusty
       dist: trusty
 script:
 script:
-  - make elvis
+  - '[ "$TRAVIS_OTP_RELEASE" = "18.3" ] || make elvis' # TODO: remove the guard when OTP18 support is dropped
   - make test
   - make test
+  - make edoc
   - make dialyzer
   - make dialyzer

+ 39 - 0
CHANGES

@@ -1,3 +1,42 @@
+In 4.4.0
+
+* Guards are now added to avoid silent integer truncation for numeric and
+  numeric range datatype codecs. So, an attempt to encode 100000 as `int2`
+  will now crash the connection instead of silently truncating it. #218
+* `epgsql{a,i}:cancel/1` API was documented. #224
+* Version of `execute_batch` that uses the same SQL query for each request
+  in a batch. Very convenient for batch-inserts. #209
+* It's now possible to provide `#statement{}` to `prepared_query/3`. This way
+  of calling it eliminates extra `describe` round-trip thus making it more
+  efficient. #207
+* Representation of SQL `NULL` is now fully configurable. You can choose what
+  set of Erlang terms should be interpreted as `NULL` and which term to use to
+  represent `NULL`s received from database. #212
+* It's now possible to choose between 3 representations of a `hstore` datatype:
+  map(), jiffy-style objects (default) and proplist. It can also take `map()` as
+  input now. NULL value representation is also configurable. #217
+* Edocs build was fixed. Just run `rebar3 edoc` and reference documentation for
+  all modules will be generated. But it's considered to be more "internal"
+  documentation for those who want to learn more about epgsql internals or
+  to do some hacking. It complements, but not replaces README. #214
+* `epgsql:connect` `timeout` option is more strict now - it limits TCP and SSL
+  setup time as a whole. #223
+* Test coverage report was enabled in CI. We will fail the build if coverage
+  falls below 55%. We hope to improve this metric over time. #208
+* We now send `Terminate` message to the server when doing graceful connection
+  shutdown (as recommended by protocol). #219
+* We found that `describe(_, portal, _)` API was broken since release v4.0.0, but
+  was not covered by tests. So now it was fixed and tests were added. #211
+* Error code to error name conversion code was updated (see `#error.codename`).
+  Some new codes were added (mostly related to JSON datatypes) and one has changed.
+  So, if you were matching over `#error.codename` being
+  `invalid_preceding_following_size` you have to update your code. #210
+* `#column{}` record is now fully documented. It was extended to
+  include `table_oid` and `table_attr_number` fields which point to the originating
+  database table of this column (if any). #205
+* Extended timerange datatype support #204
+* Some minor typos, datatype and CI fixes #199 #201 #206 #221
+
 In 4.3.0
 In 4.3.0
 
 
 * Erlang 22 compatibility is tested; support for Erlang 17 was dropped. Last
 * Erlang 22 compatibility is tested; support for Erlang 17 was dropped. Last

+ 20 - 3
Makefile

@@ -1,4 +1,5 @@
 REBAR = ./rebar3
 REBAR = ./rebar3
+MINIMAL_COVERAGE = 55
 
 
 all: compile
 all: compile
 
 
@@ -11,12 +12,25 @@ compile: src/epgsql_errcodes.erl $(REBAR)
 
 
 clean: $(REBAR)
 clean: $(REBAR)
 	@$(REBAR) clean
 	@$(REBAR) clean
+	@rm -f doc/*.html
+	@rm -f doc/erlang.png
+	@rm -f doc/stylesheet.css
+	@rm -f doc/edoc-info
 
 
 src/epgsql_errcodes.erl:
 src/epgsql_errcodes.erl:
 	./generate_errcodes_src.sh > src/epgsql_errcodes.erl
 	./generate_errcodes_src.sh > src/epgsql_errcodes.erl
 
 
-test: compile
-	@$(REBAR) do ct -v
+common-test:
+	$(REBAR) ct -v -c
+
+eunit:
+	$(REBAR) eunit -c
+
+# Fail the build if coverage falls below 55%
+cover:
+	$(REBAR) cover -v --min_coverage $(MINIMAL_COVERAGE)
+
+test: compile eunit common-test cover
 
 
 dialyzer: compile
 dialyzer: compile
 	@$(REBAR) dialyzer
 	@$(REBAR) dialyzer
@@ -24,4 +38,7 @@ dialyzer: compile
 elvis: $(REBAR)
 elvis: $(REBAR)
 	@$(REBAR) as lint lint
 	@$(REBAR) as lint lint
 
 
-.PHONY: all compile clean test dialyzer elvis
+edoc: $(REBAR)
+	@$(REBAR) edoc
+
+.PHONY: all compile clean common-test eunit cover test dialyzer elvis

+ 115 - 39
README.md

@@ -74,6 +74,7 @@ connect(Opts) -> {ok, Connection :: epgsql:connection()} | {error, Reason :: epg
       timeout =>  timeout(),             % socket connect timeout, default: 5000 ms
       timeout =>  timeout(),             % socket connect timeout, default: 5000 ms
       async =>    pid() | atom(),        % process to receive LISTEN/NOTIFY msgs
       async =>    pid() | atom(),        % process to receive LISTEN/NOTIFY msgs
       codecs =>   [{epgsql_codec:codec_mod(), any()}]}
       codecs =>   [{epgsql_codec:codec_mod(), any()}]}
+      nulls =>    [any(), ...],          % NULL terms
       replication => Replication :: string()} % Pass "database" to connect in replication mode
       replication => Replication :: string()} % Pass "database" to connect in replication mode
     | list().
     | list().
 
 
@@ -96,14 +97,20 @@ Only `host` and `username` are mandatory, but most likely you would need `databa
 - `password` - DB user password. It might be provided as string / binary or as a fun that returns
 - `password` - DB user password. It might be provided as string / binary or as a fun that returns
    string / binary. Internally, plain password is wrapped to anonymous fun before it is sent to connection
    string / binary. Internally, plain password is wrapped to anonymous fun before it is sent to connection
    process, so, if `connect` command crashes, plain password will not appear in crash logs.
    process, so, if `connect` command crashes, plain password will not appear in crash logs.
-- `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
-   socket fails to connect within `TimeoutMs` milliseconds.
+- `timeout` parameter will trigger an `{error, timeout}` result when the
+   socket fails to connect within provided milliseconds.
 - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted
 - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted
   if encryption isn't supported by server. if set to `required` connection will fail if encryption
   if encryption isn't supported by server. if set to `required` connection will fail if encryption
   is not available.
   is not available.
 - `ssl_opts` will be passed as is to `ssl:connect/3`
 - `ssl_opts` will be passed as is to `ssl:connect/3`
 - `async` see [Server notifications](#server-notifications)
 - `async` see [Server notifications](#server-notifications)
 - `codecs` see [Pluggable datatype codecs](#pluggable-datatype-codecs)
 - `codecs` see [Pluggable datatype codecs](#pluggable-datatype-codecs)
+- `nulls` terms which will be used to represent SQL `NULL`. If any of those has been encountered in
+   placeholder parameters (`$1`, `$2` etc values), it will be interpreted as `NULL`.
+   1st element of the list will be used to represent NULLs received from the server. It's not recommended
+   to use `"string"`s or lists. Try to keep this list short for performance!
+   Default is `[null, undefined]`, i.e. encode `null` or `undefined` in parameters as `NULL`
+   and decode `NULL`s as atom `null`.
 - `replication` see [Streaming replication protocol](#streaming-replication-protocol)
 - `replication` see [Streaming replication protocol](#streaming-replication-protocol)
 
 
 Options may be passed as proplist or as map with the same key names.
 Options may be passed as proplist or as map with the same key names.
@@ -126,21 +133,15 @@ Asynchronous connect example (applies to **epgsqli** too):
 ### Simple Query
 ### Simple Query
 
 
 ```erlang
 ```erlang
--type query() :: string() | iodata().
--type squery_row() :: {binary()}.
+-include_lib("epgsql/include/epgsql.hrl").
 
 
--record(column, {
-    name :: binary(),
-    type :: epgsql_type(),
-    size :: -1 | pos_integer(),
-    modifier :: -1 | pos_integer(),
-    format :: integer()
-}).
+-type query() :: string() | iodata().
+-type squery_row() :: tuple() % tuple of binary().
 
 
 -type ok_reply(RowType) ::
 -type ok_reply(RowType) ::
-    {ok, ColumnsDescription :: [#column{}], RowsValues :: [RowType]} |                            % select
+    {ok, ColumnsDescription :: [epgsql:column()], RowsValues :: [RowType]} |                            % select
     {ok, Count :: non_neg_integer()} |                                                            % update/insert/delete
     {ok, Count :: non_neg_integer()} |                                                            % update/insert/delete
-    {ok, Count :: non_neg_integer(), ColumnsDescription :: [#column{}], RowsValues :: [RowType]}. % update/insert/delete + returning
+    {ok, Count :: non_neg_integer(), ColumnsDescription :: [epgsql:column()], RowsValues :: [RowType]}. % update/insert/delete + returning
 -type error_reply() :: {error, query_error()}.
 -type error_reply() :: {error, query_error()}.
 -type reply(RowType) :: ok_reply() | error_reply().
 -type reply(RowType) :: ok_reply() | error_reply().
 
 
@@ -159,7 +160,7 @@ epgsql:squery(C, "insert into account (name) values  ('alice'), ('bob')").
 ```erlang
 ```erlang
 epgsql:squery(C, "select * from account").
 epgsql:squery(C, "select * from account").
 > {ok,
 > {ok,
-    [{column,<<"id">>,int4,4,-1,0},{column,<<"name">>,text,-1,-1,0}],
+    [#column{name = <<"id">>, type = int4, …},#column{name = <<"name">>, type = text, …}],
     [{<<"1">>,<<"alice">>},{<<"2">>,<<"bob">>}]
     [{<<"1">>,<<"alice">>},{<<"2">>,<<"bob">>}]
 }
 }
 ```
 ```
@@ -170,13 +171,12 @@ epgsql:squery(C,
     "    values ('joe'), (null)"
     "    values ('joe'), (null)"
     "    returning *").
     "    returning *").
 > {ok,2,
 > {ok,2,
-    [{column,<<"id">>,int4,4,-1,0}, {column,<<"name">>,text,-1,-1,0}],
+    [#column{name = <<"id">>, type = int4, …}, #column{name = <<"name">>, type = text, …}],
     [{<<"3">>,<<"joe">>},{<<"4">>,null}]
     [{<<"3">>,<<"joe">>},{<<"4">>,null}]
 }
 }
 ```
 ```
 
 
 ```erlang
 ```erlang
--include_lib("epgsql/include/epgsql.hrl").
 epgsql:squery(C, "SELECT * FROM _nowhere_").
 epgsql:squery(C, "SELECT * FROM _nowhere_").
 > {error,
 > {error,
    #error{severity = error,code = <<"42P01">>,
    #error{severity = error,code = <<"42P01">>,
@@ -253,7 +253,7 @@ an error occurs, all statements result in `{error, #error{}}`.
 ```erlang
 ```erlang
 epgsql:equery(C, "select id from account where name = $1", ["alice"]),
 epgsql:equery(C, "select id from account where name = $1", ["alice"]),
 > {ok,
 > {ok,
-    [{column,<<"id">>,int4,4,-1,1}],
+    [#column{name = <<"id">>, type = int4, …}],
     [{1}]
     [{1}]
 }
 }
 ```
 ```
@@ -283,25 +283,26 @@ squery including final `{C, Ref, done}`.
 ### Prepared Query
 ### Prepared Query
 
 
 ```erlang
 ```erlang
-{ok, Columns, Rows}        = epgsql:prepared_query(C, StatementName, [Parameters]).
-{ok, Count}                = epgsql:prepared_query(C, StatementName, [Parameters]).
-{ok, Count, Columns, Rows} = epgsql:prepared_query(C, StatementName, [Parameters]).
+{ok, Columns, Rows}        = epgsql:prepared_query(C, Statement :: #statement{} | string(), [Parameters]).
+{ok, Count}                = epgsql:prepared_query(C, Statement, [Parameters]).
+{ok, Count, Columns, Rows} = epgsql:prepared_query(C, Statement, [Parameters]).
 {error, Error}             = epgsql:prepared_query(C, "non_existent_query", [Parameters]).
 {error, Error}             = epgsql:prepared_query(C, "non_existent_query", [Parameters]).
 ```
 ```
 
 
-`Parameters` - optional list of values to be bound to `$1`, `$2`, `$3`, etc.
-`StatementName` - name of query given with ```erlang epgsql:parse(C, StatementName, "select ...", []).```
+- `Parameters` - optional list of values to be bound to `$1`, `$2`, `$3`, etc.
+- `Statement` - name of query given with ```erlang epgsql:parse(C, StatementName, "select ...", []).```
+               (can be empty string) or `#statement{}` record returned by `epgsql:parse`.
 
 
 With prepared query one can parse a query giving it a name with `epgsql:parse` on start and reuse the name
 With prepared query one can parse a query giving it a name with `epgsql:parse` on start and reuse the name
 for all further queries with different parameters.
 for all further queries with different parameters.
 
 
 ```erlang
 ```erlang
-epgsql:parse(C, "inc", "select $1+1", []).
-epgsql:prepared_query(C, "inc", [4]).
-epgsql:prepared_query(C, "inc", [1]).
+{ok, Stmt} = epgsql:parse(C, "inc", "select $1+1", []).
+epgsql:prepared_query(C, Stmt, [4]).
+epgsql:prepared_query(C, Stmt, [1]).
 ```
 ```
 
 
-Asynchronous API `epgsqla:prepared_query/3` requires you to parse statement beforehand
+Asynchronous API `epgsqla:prepared_query/3` requires you to always parse statement beforehand
 
 
 ```erlang
 ```erlang
 #statement{types = Types} = Statement,
 #statement{types = Types} = Statement,
@@ -384,23 +385,54 @@ Batch execution is `bind` + `execute` for several prepared statements.
 It uses unnamed portals and `MaxRows = 0`.
 It uses unnamed portals and `MaxRows = 0`.
 
 
 ```erlang
 ```erlang
-Results = epgsql:execute_batch(C, Batch).
+Results = epgsql:execute_batch(C, BatchStmt :: [{statement(), [bind_param()]}]).
+{Columns, Results} = epgsql:execute_batch(C, statement() | sql_query(), Batch :: [ [bind_param()] ]).
 ```
 ```
 
 
-- `Batch`   - list of {Statement, ParameterValues}
-- `Results` - list of {ok, Count} or {ok, Count, Rows}
+- `BatchStmt` - list of `{Statement, ParameterValues}`, each item has it's own `#statement{}`
+- `Batch` - list of `ParameterValues`, each item executes the same common `#statement{}` or SQL query
+- `Columns` - list of `#column{}` descriptions of `Results` columns
+- `Results` - list of `{ok, Count}` or `{ok, Count, Rows}`
+
+There are 2 versions:
+
+`execute_batch/2` - each item in a batch has it's own named statement (but it's allowed to have duplicates)
 
 
 example:
 example:
 
 
 ```erlang
 ```erlang
-{ok, S1} = epgsql:parse(C, "one", "select $1", [int4]),
-{ok, S2} = epgsql:parse(C, "two", "select $1 + $2", [int4, int4]),
+{ok, S1} = epgsql:parse(C, "one", "select $1::integer", []),
+{ok, S2} = epgsql:parse(C, "two", "select $1::integer + $2::integer", []),
 [{ok, [{1}]}, {ok, [{3}]}] = epgsql:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}]).
 [{ok, [{1}]}, {ok, [{3}]}] = epgsql:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}]).
+ok = epgsql:close(C, "one").
+ok = epgsql:close(C, "two").
+```
+
+`execute_batch/3` - each item in a batch executed with the same common SQL query or `#statement{}`.
+It's allowed to use unnamed statement.
+
+example (the most efficient way to make batch inserts with epgsql):
+
+```erlang
+{ok, Stmt} = epgsql:parse(C, "my_insert", "INSERT INTO account (name, age) VALUES ($1, $2) RETURNING id", []).
+{[#column{name = <<"id">>}], [{ok, [{1}]}, {ok, [{2}]}, {ok, [{3}]}]} =
+    epgsql:execute_batch(C, Stmt, [ ["Joe", 35], ["Paul", 26], ["Mary", 24] ]).
+ok = epgsql:close(C, "my_insert").
+```
+
+equivalent:
+
+```erlang
+epgsql:execute_batch(C, "INSERT INTO account (name, age) VALUES ($1, $2) RETURNING id",
+                     [ ["Joe", 35], ["Paul", 26], ["Mary", 24] ]).
 ```
 ```
 
 
-`epgsqla:execute_batch/3` sends `{C, Ref, Results}`
+In case one of the batch items causes an error, the result returned for this particular
+item will be `{error, #error{}}` and no more results will be produced.
 
 
-`epgsqli:execute_batch/3` sends
+`epgsqla:execute_batch/{2,3}` sends `{C, Ref, Results}`
+
+`epgsqli:execute_batch/{2,3}` sends
 
 
 - `{C, Ref, {data, Row}}`
 - `{C, Ref, {data, Row}}`
 - `{C, Ref, {error, Reason}}`
 - `{C, Ref, {error, Reason}}`
@@ -408,9 +440,45 @@ example:
 - `{C, Ref, {complete, _Type}}`
 - `{C, Ref, {complete, _Type}}`
 - `{C, Ref, done}` - execution of all queries from Batch has finished
 - `{C, Ref, done}` - execution of all queries from Batch has finished
 
 
+### Query cancellation
+
+```erlang
+epgsql:cancel(connection()) -> ok.
+```
+
+PostgreSQL protocol supports [cancellation](https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.9)
+of currently executing command. `cancel/1` sends a cancellation request via the
+new temporary TCP connection asynchronously, it doesn't await for the command to
+be cancelled. Instead, client should expect to get
+`{error, #error{code = <<"57014">>, codename = query_canceled}}` back from
+the command that was cancelled. However, normal response can still be received as well.
+While it's not so straightforward to use with synchronous `epgsql` API, it plays
+quite nicely with asynchronous `epgsqla` API. For example, that's how a query with
+soft timeout can be implemented:
+
+```erlang
+squery(C, SQL, Timeout) ->
+    Ref = epgsqla:squery(C, SQL),
+    receive
+       {C, Ref, Result} -> Result
+    after Timeout ->
+        ok = epgsql:cancel(C),
+        % We can still receive {ok, …} as well as
+        % {error, #error{codename = query_canceled}} or any other error
+        receive
+            {C, Ref, Result} -> Result
+        end
+    end.
+```
+
+This API should be used with extreme care when pipelining is in use: it only cancels
+currently executing command, all the subsequent pipelined commands will continue
+their normal execution. And it's not always easy to see which command exactly is
+executing when we are issuing the cancellation request.
+
 ## Data Representation
 ## Data Representation
 
 
-Data representation may be configured using [pluggable datatype codecs](pluggable_types.md),
+Data representation may be configured using [pluggable datatype codecs](doc/pluggable_types.md),
 so following is just default mapping:
 so following is just default mapping:
 
 
 PG type       | Representation
 PG type       | Representation
@@ -433,8 +501,8 @@ PG type       | Representation
   record      | `{int2, time, text, ...}` (decode only)
   record      | `{int2, time, text, ...}` (decode only)
   point       |  `{10.2, 100.12}`
   point       |  `{10.2, 100.12}`
   int4range   | `[1,5)`
   int4range   | `[1,5)`
-  hstore      | `{[ {binary(), binary() \| null} ]}`
-  json/jsonb  | `<<"{ \"key\": [ 1, 1.0, true, \"string\" ] }">>` (see below for codec details)
+  hstore      | `{[ {binary(), binary() \| null} ]}` (configurable)
+  json/jsonb  | `<<"{ \"key\": [ 1, 1.0, true, \"string\" ] }">>` (configurable)
   uuid        | `<<"123e4567-e89b-12d3-a456-426655440000">>`
   uuid        | `<<"123e4567-e89b-12d3-a456-426655440000">>`
   inet        | `inet:ip_address()`
   inet        | `inet:ip_address()`
   cidr        | `{ip_address(), Mask :: 0..32}`
   cidr        | `{ip_address(), Mask :: 0..32}`
@@ -444,6 +512,8 @@ PG type       | Representation
   tstzrange   | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
   tstzrange   | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
   daterange   | `{{Year, Month, Day}, {Year, Month, Day}}`
   daterange   | `{{Year, Month, Day}, {Year, Month, Day}}`
 
 
+`null` can be configured. See `nulls` `connect/1` option.
+
 `timestamp` and `timestamptz` parameters can take `erlang:now()` format: `{MegaSeconds, Seconds, MicroSeconds}`
 `timestamp` and `timestamptz` parameters can take `erlang:now()` format: `{MegaSeconds, Seconds, MicroSeconds}`
 
 
 `int4range` is a range type for ints that obeys inclusive/exclusive semantics,
 `int4range` is a range type for ints that obeys inclusive/exclusive semantics,
@@ -453,6 +523,12 @@ and `plus_infinity`
 `tsrange`, `tstzrange`, `daterange` are range types for `timestamp`, `timestamptz` and `date`
 `tsrange`, `tstzrange`, `daterange` are range types for `timestamp`, `timestamptz` and `date`
 respectively. They can return `empty` atom as the result from a database if bounds are equal
 respectively. They can return `empty` atom as the result from a database if bounds are equal
 
 
+`hstore` type can take map or jiffy-style objects as input. Output can be tuned by
+providing `return :: map | jiffy | proplist` option to choose the format to which
+hstore should be decoded. `nulls :: [atom(), ...]` option can be used to select the
+terms which should be interpreted as SQL `NULL` - semantics is the same as
+for `connect/1` `nulls` option.
+
 `json` and `jsonb` types can optionally use a custom JSON encoding/decoding module to accept
 `json` and `jsonb` types can optionally use a custom JSON encoding/decoding module to accept
 and return erlang-formatted JSON. The module must implement the callbacks in `epgsql_codec_json`,
 and return erlang-formatted JSON. The module must implement the callbacks in `epgsql_codec_json`,
 which most popular open source JSON parsers will already, and you can specify it in the codec
 which most popular open source JSON parsers will already, and you can specify it in the codec
@@ -584,15 +660,15 @@ Parameter's value may change during connection's lifetime.
 
 
 ## Streaming replication protocol
 ## Streaming replication protocol
 
 
-See [streaming.md](streaming.md).
+See [streaming.md](doc/streaming.md).
 
 
 ## Pluggable commands
 ## Pluggable commands
 
 
-See [pluggable_commands.md](pluggable_commands.md)
+See [pluggable_commands.md](doc/pluggable_commands.md)
 
 
 ## Pluggable datatype codecs
 ## Pluggable datatype codecs
 
 
-See [pluggable_types.md](pluggable_types.md)
+See [pluggable_types.md](doc/pluggable_types.md)
 
 
 ## Mailing list
 ## Mailing list
 
 

+ 57 - 0
doc/overview.edoc

@@ -0,0 +1,57 @@
+@title epgsql - PostgreSQL driver for Erlang, internal documentation
+@doc
+This document is made mostly as internal documentation. It can be useful
+if you plan to contribute some patches to epgsql, want to implement
+custom datatypes or commands or to better understand epgsql internals.
+
+End-user interface is described in <a href="https://github.com/epgsql/epgsql#readme">README.md</a>.
+
+== Interfaces ==
+Epgsql has 3 end-user API interfaces:
+
+<ul>
+  <li>{@link epgsql} - synchronous</li>
+  <li>{@link epgsqla} - asynchronous</li>
+  <li>{@link epgsqli} - incremental</li>
+</ul>
+
+== Internals ==
+
+All 3 interfaces communicate with {@link epgsql_sock} gen_server, which holds all
+the connection state. While `epgsql_sock' holds all the state, it doesn't know
+much about Client-Server communication protocol.
+All the communication logic between epgsql and PostgreSQL server is implemented
+as a {@section Commands} and `epgsql_sock' acts as an executor for those commands.
+
+PostgreSQL binary communication protocol is represented by 2 modules:
+<ul>
+  <li>{@link epgsql_wire} - codecs for on-wire communication protocol messages</li>
+  <li>{@link epgsql_binary} - interface to PostgreSQL binary data encoding protocol(see {@section Datatypes})</li>
+</ul>
+
+`epgsql_sock' holds an internal state of `epgsql_binary' codecs as well. The
+main contents of this state is the mapping between PostgreSQL unique numeric
+datatype IDs (OIDs) and callbacks which will be used to decode this datatype.
+This mapping is handled by {@link epgsql_oid_db} module and is populated at
+connection set-up time by {@link epgsql_cmd_connect}.
+
+Most of the connection initialization (network connection, authentication, codecs)
+is performed by {@link epgsql_cmd_connect} command, wich is just a regualr command
+(but quite complex one) and can be replaced by own implementation if needed.
+
+== Commands ==
+
+Client can execute a number of built-in commands as well as define their own.
+See {@link epgsql_command} and all the `epgsql_cmd_*' pages.
+There exists a <a href="pluggable_commands.md">manual</a> that explains how to
+implement your own command.
+
+== Datatypes ==
+
+Epgsql supports both PostgreSQL <a href="https://www.postgresql.org/docs/current/protocol-overview.html#PROTOCOL-FORMAT-CODES">text and binary</a>
+data encodings to transfer the data (query placeholder parameters and result rows).
+There are a bunch of built-in codecs and it's possible to
+implement custom ones as well as fine-tune some of built-ins.
+See {@link epgsql_codec} and all the `epgsql_codec_*' pages for more details.
+There exists a <a href="pluggable_types.md">manual</a> that explains how to
+implement your own datatype codec.

+ 0 - 0
pluggable_commands.md → doc/pluggable_commands.md


+ 0 - 0
pluggable_types.md → doc/pluggable_types.md


+ 0 - 0
streaming.md → doc/streaming.md


+ 3 - 3
generate_errcodes_src.sh

@@ -1,15 +1,15 @@
-#!/usr/bin/env bash -e
+#!/usr/bin/env bash
 #
 #
 # Used to generate epgsql_errcodes.erl
 # Used to generate epgsql_errcodes.erl
 #
 #
-ERRFILE="https://raw.github.com/postgres/postgres/master/src/backend/utils/errcodes.txt"
+ERRFILE="https://raw.githubusercontent.com/postgres/postgres/master/src/backend/utils/errcodes.txt"
 echo "%% DO NOT EDIT - AUTOGENERATED BY $0 ON $(date +%Y-%m-%dT%H:%M:%S%z)"
 echo "%% DO NOT EDIT - AUTOGENERATED BY $0 ON $(date +%Y-%m-%dT%H:%M:%S%z)"
 echo "-module(epgsql_errcodes)."
 echo "-module(epgsql_errcodes)."
 echo "-export([to_name/1])."
 echo "-export([to_name/1])."
 echo
 echo
 wget -qO- "$ERRFILE" | awk '
 wget -qO- "$ERRFILE" | awk '
 NF == 4 && \
 NF == 4 && \
-$1 ~ /[^\s]{5}/ && \
+$1 ~ /[0-9A_Z]+/ && \
 $2 ~ /[EWS]/ \
 $2 ~ /[EWS]/ \
 {
 {
     printf("to_name(<<\"%s\">>) -> %s;\n", $1, $4)
     printf("to_name(<<\"%s\">>) -> %s;\n", $1, $4)

+ 21 - 3
include/epgsql.hrl

@@ -1,10 +1,26 @@
+%% See https://www.postgresql.org/docs/current/protocol-message-formats.html
+%% Description of `RowDescription' packet
 -record(column, {
 -record(column, {
+    %% field name
     name :: binary(),
     name :: binary(),
+    %% name of the field data type
     type :: epgsql:epgsql_type(),
     type :: epgsql:epgsql_type(),
-    oid :: integer(),
+    %% OID of the field's data type
+    oid :: non_neg_integer(),
+    %% data type size (see pg_type.typlen). negative values denote variable-width types
     size :: -1 | pos_integer(),
     size :: -1 | pos_integer(),
+    %% type modifier (see pg_attribute.atttypmod). meaning of the modifier is type-specific
     modifier :: -1 | pos_integer(),
     modifier :: -1 | pos_integer(),
-    format :: integer()
+    %% format code being used for the field during server->client transmission.
+    %% Currently will be zero (text) or one (binary).
+    format :: integer(),
+    %% If the field can be identified as a column of a specific table, the OID of the table; otherwise zero.
+    %% SELECT relname FROM pg_catalog.pg_class WHERE oid=<table_oid>
+    table_oid :: non_neg_integer(),
+    %% If table_oid is not zero, the attribute number of the column; otherwise zero.
+    %% SELECT attname FROM pg_catalog.pg_attribute
+    %% WHERE attrelid=<table_oid> AND attnum=<table_attr_number>
+    table_attr_number :: pos_integer()
 }).
 }).
 
 
 -record(statement, {
 -record(statement, {
@@ -14,10 +30,12 @@
     parameter_info :: [epgsql_oid_db:oid_entry()]
     parameter_info :: [epgsql_oid_db:oid_entry()]
 }).
 }).
 
 
+
+%% See https://www.postgresql.org/docs/current/protocol-error-fields.html
 -record(error, {
 -record(error, {
     % see client_min_messages config option
     % see client_min_messages config option
     severity :: debug | log | info | notice | warning | error | fatal | panic,
     severity :: debug | log | info | notice | warning | error | fatal | panic,
-    code :: binary(),
+    code :: binary(), % See https://www.postgresql.org/docs/current/errcodes-appendix.html
     codename :: atom(),
     codename :: atom(),
     message :: binary(),
     message :: binary(),
     extra :: [{severity | detail | hint | position | internal_position | internal_query
     extra :: [{severity | detail | hint | position | internal_position | internal_query

+ 1 - 0
include/protocol.hrl

@@ -43,6 +43,7 @@
 -define(READY_FOR_QUERY, $Z).
 -define(READY_FOR_QUERY, $Z).
 -define(COPY_BOTH_RESPONSE, $W).
 -define(COPY_BOTH_RESPONSE, $W).
 -define(COPY_DATA, $d).
 -define(COPY_DATA, $d).
+-define(TERMINATE, $X).
 
 
 % CopyData replication messages
 % CopyData replication messages
 -define(X_LOG_DATA, $w).
 -define(X_LOG_DATA, $w).

+ 4 - 4
rebar.config

@@ -1,9 +1,8 @@
 %% -*- mode: erlang -*-
 %% -*- mode: erlang -*-
 
 
-{eunit_opts, [verbose]}.
-
 {cover_enabled, true}.
 {cover_enabled, true}.
-{cover_print_enabled, true}.
+
+{edoc_opts, [{preprocess, true}]}.
 
 
 {profiles, [
 {profiles, [
     {test, [
     {test, [
@@ -27,7 +26,8 @@
      ruleset => erl_files,
      ruleset => erl_files,
      rules =>
      rules =>
          [{elvis_style, line_length, #{limit => 120}},
          [{elvis_style, line_length, #{limit => 120}},
-          {elvis_style, god_modules, #{limit => 40}},
+          {elvis_style, god_modules, #{limit => 41}},
+          {elvis_style, dont_repeat_yourself, #{min_complexity => 11}},
           {elvis_style, state_record_and_type, disable} % epgsql_sock
           {elvis_style, state_record_and_type, disable} % epgsql_sock
          ]}
          ]}
   ]
   ]

+ 74 - 23
src/commands/epgsql_cmd_batch.erl

@@ -1,32 +1,52 @@
-%% > Bind
-%% < BindComplete
-%% > Execute
-%% < DataRow*
-%% < CommandComplete
-%% -- Repeated many times --
+%% @doc Execute multiple extended queries in a single network round-trip
+%%
+%% There are 2 kinds of interface:
+%% <ol>
+%%  <li>To execute multiple queries, each with it's own `statement()'</li>
+%%  <li>To execute multiple queries, but by binding different parameters to the
+%%  same `statement()'</li>
+%% </ol>
+%% ```
+%% > {Bind
+%% <  BindComplete
+%% >  Execute
+%% <  DataRow*
+%% <  CommandComplete}*
 %% > Sync
 %% > Sync
 %% < ReadyForQuery
 %% < ReadyForQuery
+%% '''
 -module(epgsql_cmd_batch).
 -module(epgsql_cmd_batch).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).
--export_type([response/0]).
+-export_type([arguments/0, response/0]).
 
 
--type response() :: [{ok, Count :: non_neg_integer(), Rows :: [tuple()]}
-                     | {ok, Count :: non_neg_integer()}
-                     | {ok, Rows :: [tuple()]}
-                     | {error, epgsql:query_error()}].
 
 
 -include("epgsql.hrl").
 -include("epgsql.hrl").
 -include("protocol.hrl").
 -include("protocol.hrl").
 
 
 -record(batch,
 -record(batch,
-        {batch :: [{#statement{}, list()}],
-         decoder}).
+        {batch :: [ [epgsql:bind_param()] ] | [{#statement{}, [epgsql:bind_param()]}],
+         statement :: #statement{} | undefined,
+         decoder :: epgsql_wire:row_decoder() | undefined}).
+
+-type arguments() ::
+        {epgsql:statement(), [ [epgsql:bind_param()] ]} |
+        [{epgsql:statement(), [epgsql:bind_param()]}].
 
 
-init(Batch) ->
+-type response() :: [{ok, Count :: non_neg_integer(), Rows :: [tuple()]}
+                     | {ok, Count :: non_neg_integer()}
+                     | {ok, Rows :: [tuple()]}
+                     | {error, epgsql:query_error()}].
+-type state() :: #batch{}.
+
+-spec init(arguments()) -> state().
+init({#statement{} = Statement, Batch}) ->
+    #batch{statement = Statement,
+           batch = Batch};
+init(Batch) when is_list(Batch) ->
     #batch{batch = Batch}.
     #batch{batch = Batch}.
 
 
-execute(Sock, #batch{batch = Batch} = State) ->
+execute(Sock, #batch{batch = Batch, statement = undefined} = State) ->
     Codec = epgsql_sock:get_codec(Sock),
     Codec = epgsql_sock:get_codec(Sock),
     Commands =
     Commands =
         lists:foldr(
         lists:foldr(
@@ -34,19 +54,38 @@ execute(Sock, #batch{batch = Batch} = State) ->
                   #statement{name = StatementName,
                   #statement{name = StatementName,
                              columns = Columns,
                              columns = Columns,
                              types = Types} = Statement,
                              types = Types} = Statement,
-                  TypedParameters = lists:zip(Types, Parameters),
-                  Bin1 = epgsql_wire:encode_parameters(TypedParameters, Codec),
-                  Bin2 = epgsql_wire:encode_formats(Columns),
-                  [{?BIND, [0, StatementName, 0, Bin1, Bin2]},
-                   {?EXECUTE, [0, <<0:?int32>>]} | Acc]
+                  BinFormats = epgsql_wire:encode_formats(Columns),
+                  add_command(StatementName, Types, Parameters, BinFormats, Codec, Acc)
+          end,
+          [{?SYNC, []}],
+          Batch),
+    epgsql_sock:send_multi(Sock, Commands),
+    {ok, Sock, State};
+execute(Sock, #batch{batch = Batch,
+                     statement = #statement{name = StatementName,
+                                            columns = Columns,
+                                            types = Types}} = State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    BinFormats = epgsql_wire:encode_formats(Columns),
+    %% TODO: build some kind of encoder and reuse it for each batch item
+    Commands =
+        lists:foldr(
+          fun(Parameters, Acc) ->
+                  add_command(StatementName, Types, Parameters, BinFormats, Codec, Acc)
           end,
           end,
           [{?SYNC, []}],
           [{?SYNC, []}],
           Batch),
           Batch),
     epgsql_sock:send_multi(Sock, Commands),
     epgsql_sock:send_multi(Sock, Commands),
     {ok, Sock, State}.
     {ok, Sock, State}.
 
 
-handle_message(?BIND_COMPLETE, <<>>, Sock, #batch{batch = [{Stmt, _} | _]} = State) ->
-    #statement{columns = Columns} = Stmt,
+add_command(StmtName, Types, Params, BinFormats, Codec, Acc) ->
+    TypedParameters = lists:zip(Types, Params),
+    BinParams = epgsql_wire:encode_parameters(TypedParameters, Codec),
+    [{?BIND, [0, StmtName, 0, BinParams, BinFormats]},
+     {?EXECUTE, [0, <<0:?int32>>]} | Acc].
+
+handle_message(?BIND_COMPLETE, <<>>, Sock, State) ->
+    Columns = current_cols(State),
     Codec = epgsql_sock:get_codec(Sock),
     Codec = epgsql_sock:get_codec(Sock),
     Decoder = epgsql_wire:build_decoder(Columns, Codec),
     Decoder = epgsql_wire:build_decoder(Columns, Codec),
     {noaction, Sock, State#batch{decoder = Decoder}};
     {noaction, Sock, State#batch{decoder = Decoder}};
@@ -58,7 +97,8 @@ handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>, Sock,
 %%     Sock1 = epgsql_sock:add_result(Sock, {complete, empty}, {ok, [], []}),
 %%     Sock1 = epgsql_sock:add_result(Sock, {complete, empty}, {ok, [], []}),
 %%     {noaction, Sock1};
 %%     {noaction, Sock1};
 handle_message(?COMMAND_COMPLETE, Bin, Sock,
 handle_message(?COMMAND_COMPLETE, Bin, Sock,
-               #batch{batch = [{#statement{columns = Columns}, _} | Batch]} = State) ->
+               #batch{batch = [_ | Batch]} = State) ->
+    Columns = current_cols(State),
     Complete = epgsql_wire:decode_complete(Bin),
     Complete = epgsql_wire:decode_complete(Bin),
     Rows = epgsql_sock:get_rows(Sock),
     Rows = epgsql_sock:get_rows(Sock),
     Result = case Complete of
     Result = case Complete of
@@ -79,3 +119,14 @@ handle_message(?ERROR, Error, Sock, #batch{batch = [_ | Batch]} = State) ->
     {add_result, Result, Result, Sock, State#batch{batch = Batch}};
     {add_result, Result, Result, Sock, State#batch{batch = Batch}};
 handle_message(_, _, _, _) ->
 handle_message(_, _, _, _) ->
     unknown.
     unknown.
+
+%% Helpers
+
+current_cols(Batch) ->
+    #statement{columns = Columns} = current_stmt(Batch),
+    Columns.
+
+current_stmt(#batch{batch = [{Stmt, _} | _], statement = undefined}) ->
+    Stmt;
+current_stmt(#batch{statement = #statement{} = Stmt}) ->
+    Stmt.

+ 6 - 0
src/commands/epgsql_cmd_bind.erl

@@ -1,5 +1,11 @@
+%% @doc Binds placeholder parameters to prepared statement
+%%
+%% ```
 %% > Bind
 %% > Bind
 %% < BindComplete
 %% < BindComplete
+%% '''
+%% @see epgsql_cmd_parse
+%% @see epgsql_cmd_execute
 -module(epgsql_cmd_bind).
 -module(epgsql_cmd_bind).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 4 - 0
src/commands/epgsql_cmd_close.erl

@@ -1,5 +1,9 @@
+%% @doc Closes statement / portal
+%%
+%% ```
 %% > Close
 %% > Close
 %% < CloseComplete
 %% < CloseComplete
+%% '''
 -module(epgsql_cmd_close).
 -module(epgsql_cmd_close).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 30 - 19
src/commands/epgsql_cmd_connect.erl

@@ -1,6 +1,8 @@
+%%% @doc Connects to the server and performs all the necessary handshakes.
+%%%
 %%% Special kind of command - it's exclusive: no other commands can run until
 %%% Special kind of command - it's exclusive: no other commands can run until
 %%% this one finishes.
 %%% this one finishes.
-%%% It also uses some 'private' epgsql_sock's APIs
+%%% It also uses some "private" epgsql_sock's APIs
 %%%
 %%%
 -module(epgsql_cmd_connect).
 -module(epgsql_cmd_connect).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
@@ -47,19 +49,20 @@ init(#{host := _, username := _} = Opts) ->
 
 
 execute(PgSock, #connect{opts = #{host := Host} = Opts, stage = connect} = State) ->
 execute(PgSock, #connect{opts = #{host := Host} = Opts, stage = connect} = State) ->
     Timeout = maps:get(timeout, Opts, 5000),
     Timeout = maps:get(timeout, Opts, 5000),
+    Deadline = deadline(Timeout),
     Port = maps:get(port, Opts, 5432),
     Port = maps:get(port, Opts, 5432),
     SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}, {keepalive, true}],
     SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}, {keepalive, true}],
     case gen_tcp:connect(Host, Port, SockOpts, Timeout) of
     case gen_tcp:connect(Host, Port, SockOpts, Timeout) of
         {ok, Sock} ->
         {ok, Sock} ->
-            client_handshake(Sock, PgSock, State);
+            client_handshake(Sock, PgSock, State, Deadline);
         {error, Reason} = Error ->
         {error, Reason} = Error ->
             {stop, Reason, Error, PgSock}
             {stop, Reason, Error, PgSock}
     end;
     end;
 execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
 execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
-    epgsql_sock:send(PgSock, PacketId, Data),
+    ok = epgsql_sock:send(PgSock, PacketId, Data),
     {ok, PgSock, St#connect{auth_send = undefined}}.
     {ok, PgSock, St#connect{auth_send = undefined}}.
 
 
-client_handshake(Sock, PgSock, #connect{opts = #{username := Username} = Opts} = State) ->
+client_handshake(Sock, PgSock, #connect{opts = #{username := Username} = Opts} = State, Deadline) ->
     %% Increase the buffer size.  Following the recommendation in the inet man page:
     %% Increase the buffer size.  Following the recommendation in the inet man page:
     %%
     %%
     %%    It is recommended to have val(buffer) >=
     %%    It is recommended to have val(buffer) >=
@@ -69,7 +72,7 @@ client_handshake(Sock, PgSock, #connect{opts = #{username := Username} = Opts} =
         inet:getopts(Sock, [recbuf, sndbuf]),
         inet:getopts(Sock, [recbuf, sndbuf]),
     inet:setopts(Sock, [{buffer, max(RecBufSize, SndBufSize)}]),
     inet:setopts(Sock, [{buffer, max(RecBufSize, SndBufSize)}]),
 
 
-    case maybe_ssl(Sock, maps:get(ssl, Opts, false), Opts, PgSock) of
+    case maybe_ssl(Sock, maps:get(ssl, Opts, false), Opts, PgSock, Deadline) of
         {error, Reason} ->
         {error, Reason} ->
             {stop, Reason, {error, Reason}, PgSock};
             {stop, Reason, {error, Reason}, PgSock};
         PgSock1 ->
         PgSock1 ->
@@ -86,8 +89,7 @@ client_handshake(Sock, PgSock, #connect{opts = #{username := Username} = Opts} =
                          epgsql_sock:init_replication_state(PgSock1)};
                          epgsql_sock:init_replication_state(PgSock1)};
                     _ -> {Opts3, PgSock1}
                     _ -> {Opts3, PgSock1}
                 end,
                 end,
-
-            epgsql_sock:send(PgSock2, [<<196608:?int32>>, Opts4, 0]),
+            ok = epgsql_sock:send(PgSock2, [<<196608:?int32>>, Opts4, 0]),
             PgSock3 = case Opts of
             PgSock3 = case Opts of
                           #{async := Async} ->
                           #{async := Async} ->
                               epgsql_sock:set_attr(async, Async, PgSock2);
                               epgsql_sock:set_attr(async, Async, PgSock2);
@@ -105,7 +107,7 @@ opts_hide_password(Opts) -> Opts.
 
 
 
 
 %% @doc this function wraps plaintext password to a lambda function, so, if
 %% @doc this function wraps plaintext password to a lambda function, so, if
-%% epgsql_sock process crashes when executing `connect` command, password will
+%% epgsql_sock process crashes when executing `connect' command, password will
 %% not appear in a crash log
 %% not appear in a crash log
 -spec hide_password(iodata()) -> fun( () -> iodata() ).
 -spec hide_password(iodata()) -> fun( () -> iodata() ).
 hide_password(Password) when is_list(Password);
 hide_password(Password) when is_list(Password);
@@ -117,15 +119,15 @@ hide_password(PasswordFun) when is_function(PasswordFun, 0) ->
     PasswordFun.
     PasswordFun.
 
 
 
 
-maybe_ssl(S, false, _, PgSock) ->
+maybe_ssl(S, false, _, PgSock, _Deadline) ->
     epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
     epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
-maybe_ssl(S, Flag, Opts, PgSock) ->
+maybe_ssl(S, Flag, Opts, PgSock, Deadline) ->
     ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>),
     ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>),
-    Timeout = maps:get(timeout, Opts, 5000),
-    {ok, <<Code>>} = gen_tcp:recv(S, 1, Timeout),
-    case Code of
-        $S  ->
+    Timeout0 = timeout(Deadline),
+    case gen_tcp:recv(S, 1, Timeout0) of
+        {ok, <<$S>>}  ->
             SslOpts = maps:get(ssl_opts, Opts, []),
             SslOpts = maps:get(ssl_opts, Opts, []),
+            Timeout = timeout(Deadline),
             case ssl:connect(S, SslOpts, Timeout) of
             case ssl:connect(S, SslOpts, Timeout) of
                 {ok, S2}        ->
                 {ok, S2}        ->
                     epgsql_sock:set_net_socket(ssl, S2, PgSock);
                     epgsql_sock:set_net_socket(ssl, S2, PgSock);
@@ -133,13 +135,15 @@ maybe_ssl(S, Flag, Opts, PgSock) ->
                     Err = {ssl_negotiation_failed, Reason},
                     Err = {ssl_negotiation_failed, Reason},
                     {error, Err}
                     {error, Err}
             end;
             end;
-        $N ->
+        {ok, <<$N>>} ->
             case Flag of
             case Flag of
                 true ->
                 true ->
                     epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
                     epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
                 required ->
                 required ->
                     {error, ssl_not_available}
                     {error, ssl_not_available}
-            end
+            end;
+        {error, Reason} ->
+            {error, Reason}
     end.
     end.
 
 
 %% Auth sub-protocol
 %% Auth sub-protocol
@@ -226,7 +230,7 @@ auth_scram(_, _, _) ->
 handle_message(?AUTHENTICATION_REQUEST, <<?AUTH_OK:?int32>>, Sock, State) ->
 handle_message(?AUTHENTICATION_REQUEST, <<?AUTH_OK:?int32>>, Sock, State) ->
     {noaction, Sock, State#connect{stage = initialization,
     {noaction, Sock, State#connect{stage = initialization,
                                    auth_fun = undefined,
                                    auth_fun = undefined,
-                                   auth_state = undefned,
+                                   auth_state = undefined,
                                    auth_send = undefined}};
                                    auth_send = undefined}};
 
 
 handle_message(?AUTHENTICATION_REQUEST, Message, Sock, #connect{stage = Stage} = St) when Stage =/= auth ->
 handle_message(?AUTHENTICATION_REQUEST, Message, Sock, #connect{stage = Stage} = St) when Stage =/= auth ->
@@ -242,8 +246,9 @@ handle_message(?CANCELLATION_KEY, <<Pid:?int32, Key:?int32>>, Sock, _State) ->
     {noaction, epgsql_sock:set_attr(backend, {Pid, Key}, Sock)};
     {noaction, epgsql_sock:set_attr(backend, {Pid, Key}, Sock)};
 
 
 %% ReadyForQuery
 %% ReadyForQuery
-handle_message(?READY_FOR_QUERY, _, Sock, _State) ->
-    Codec = epgsql_binary:new_codec(Sock, []),
+handle_message(?READY_FOR_QUERY, _, Sock, #connect{opts = Opts}) ->
+    CodecOpts = maps:with([nulls], Opts),
+    Codec = epgsql_binary:new_codec(Sock, CodecOpts),
     Sock1 = epgsql_sock:set_attr(codec, Codec, Sock),
     Sock1 = epgsql_sock:set_attr(codec, Codec, Sock),
     {finish, connected, connected, Sock1};
     {finish, connected, connected, Sock1};
 
 
@@ -274,3 +279,9 @@ hex(Bin) ->
                (N) when N < 16 -> $W + N
                (N) when N < 16 -> $W + N
             end,
             end,
     <<<<(HChar(H)), (HChar(L))>> || <<H:4, L:4>> <= Bin>>.
     <<<<(HChar(H)), (HChar(L))>> || <<H:4, L:4>> <= Bin>>.
+
+deadline(Timeout) ->
+    erlang:monotonic_time(milli_seconds) + Timeout.
+
+timeout(Deadline) ->
+    erlang:max(0, Deadline - erlang:monotonic_time(milli_seconds)).

+ 9 - 5
src/commands/epgsql_cmd_describe_portal.erl

@@ -1,5 +1,9 @@
-%% > Describe
+%% @doc Asks the server to provide description of portal's results columns
+%%
+%% ```
+%% > Describe(PORTAL)
 %% < RowDescription | NoData
 %% < RowDescription | NoData
+%% '''
 -module(epgsql_cmd_describe_portal).
 -module(epgsql_cmd_describe_portal).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).
@@ -26,12 +30,12 @@ execute(Sock, #desc_portal{name = Name} = St) ->
       ]),
       ]),
     {ok, Sock, St}.
     {ok, Sock, St}.
 
 
-handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock, St) ->
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock, _St) ->
     Codec = epgsql_sock:get_codec(Sock),
     Codec = epgsql_sock:get_codec(Sock),
     Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
     Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
-    {finish, {ok, Columns}, {columns, Columns}, St};
-handle_message(?NO_DATA, <<>>, _Sock, _State) ->
-    {finish, {ok, []}, no_data};
+    {finish, {ok, Columns}, {columns, Columns}, Sock};
+handle_message(?NO_DATA, <<>>, Sock, _State) ->
+    {finish, {ok, []}, no_data, Sock};
 handle_message(?ERROR, Error, _Sock, _State) ->
 handle_message(?ERROR, Error, _Sock, _State) ->
     Result = {error, Error},
     Result = {error, Error},
     {sync_required, Result};
     {sync_required, Result};

+ 7 - 2
src/commands/epgsql_cmd_describe_statement.erl

@@ -1,7 +1,12 @@
-%% Almost the same as "parse"
-%% > Describe
+%% @doc Asks server to provide input parameter and result rows information.
+%%
+%% Almost the same as {@link epgsql_cmd_parse}.
+%%
+%% ```
+%% > Describe(STATEMENT)
 %% < ParameterDescription
 %% < ParameterDescription
 %% < RowDescription | NoData
 %% < RowDescription | NoData
+%% '''
 -module(epgsql_cmd_describe_statement).
 -module(epgsql_cmd_describe_statement).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 9 - 0
src/commands/epgsql_cmd_equery.erl

@@ -1,3 +1,10 @@
+%% @doc Performs 2nd stage of
+%% <a href="https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY">
+%% extended query protocol.</a>
+%%
+%% Takes prepared `statement()' and bind-parameters for placeholders and produces
+%% query results.
+%% ```
 %% > Bind
 %% > Bind
 %% < BindComplete
 %% < BindComplete
 %% > Execute
 %% > Execute
@@ -7,6 +14,8 @@
 %% < CloseComplete
 %% < CloseComplete
 %% > Sync
 %% > Sync
 %% < ReadyForQuery
 %% < ReadyForQuery
+%% '''
+%% @see epgsql_cmd_parse
 -module(epgsql_cmd_equery).
 -module(epgsql_cmd_equery).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 8 - 0
src/commands/epgsql_cmd_execute.erl

@@ -1,6 +1,14 @@
+%% @doc Executes a portal.
+%%
+%% It's possible to tell the server to only return limited number of rows by
+%% providing non-zero `MaxRows' parameter.
+%% ```
 %% > Execute
 %% > Execute
 %% < DataRow*
 %% < DataRow*
 %% < CommandComplete | PortalSuspended
 %% < CommandComplete | PortalSuspended
+%% '''
+%% @see epgsql_cmd_parse
+%% @see epgsql_cmd_bind
 -module(epgsql_cmd_execute).
 -module(epgsql_cmd_execute).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 4 - 0
src/commands/epgsql_cmd_parse.erl

@@ -1,8 +1,12 @@
+%% @doc Asks server to parse SQL query and send information aboud bind-parameters and result columns.
+%%
+%% ```
 %% > Parse
 %% > Parse
 %% < ParseComplete
 %% < ParseComplete
 %% > Describe
 %% > Describe
 %% < ParameterDescription
 %% < ParameterDescription
 %% < RowDescription | NoData
 %% < RowDescription | NoData
+%% '''
 -module(epgsql_cmd_parse).
 -module(epgsql_cmd_parse).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 5 - 1
src/commands/epgsql_cmd_prepared_query.erl

@@ -1,4 +1,7 @@
-%% Almost the same as equery, but don't execute 'CLOSE'
+%% @doc Almost the same as equery, but don't execute 'CLOSE'
+%%
+%% So, statement can be reused multiple times.
+%% ```
 %% > Bind
 %% > Bind
 %% < BindComplete
 %% < BindComplete
 %% > Execute
 %% > Execute
@@ -6,6 +9,7 @@
 %% < CommandComplete
 %% < CommandComplete
 %% > Sync
 %% > Sync
 %% < ReadyForQuery
 %% < ReadyForQuery
+%% '''
 -module(epgsql_cmd_prepared_query).
 -module(epgsql_cmd_prepared_query).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 7 - 0
src/commands/epgsql_cmd_squery.erl

@@ -1,4 +1,10 @@
+%% @doc Executes SQL query(es) using
+%% <a href="https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.4">
+%% simple query protocol</a>
+%%
+%% Squery can not have placeholders.
 %% Squery may contain many semicolon-separated queries
 %% Squery may contain many semicolon-separated queries
+%% ```
 %% > Query
 %% > Query
 %% < (RowDescription?
 %% < (RowDescription?
 %% <  DataRow*
 %% <  DataRow*
@@ -8,6 +14,7 @@
 %% > Query when len(strip(Query)) == 0
 %% > Query when len(strip(Query)) == 0
 %% < EmptyQueryResponse
 %% < EmptyQueryResponse
 %% < ReadyForQuery
 %% < ReadyForQuery
+%% '''
 -module(epgsql_cmd_squery).
 -module(epgsql_cmd_squery).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 5 - 0
src/commands/epgsql_cmd_start_replication.erl

@@ -1,5 +1,10 @@
+%% @doc Requests server to start sending replication packets
+%%
+%% See {@link epgsql:connect/1} `replication' parameter.
+%% ```
 %% > SimpleQuery "START_REPLICATION ..."
 %% > SimpleQuery "START_REPLICATION ..."
 %% < CopyBothResponse | Error
 %% < CopyBothResponse | Error
+%% '''
 -module(epgsql_cmd_start_replication).
 -module(epgsql_cmd_start_replication).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 5 - 0
src/commands/epgsql_cmd_sync.erl

@@ -1,5 +1,10 @@
+%% @doc Synchronize client and server states for multi-command combinations
+%%
+%% Should be executed if APIs start to return `{error, sync_required}'.
+%% ```
 %% > Sync
 %% > Sync
 %% < ReadyForQuery
 %% < ReadyForQuery
+%% '''
 -module(epgsql_cmd_sync).
 -module(epgsql_cmd_sync).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 1 - 1
src/commands/epgsql_cmd_update_type_cache.erl

@@ -1,4 +1,4 @@
-%% Special command. Executes Squery over pg_type table and updates codecs.
+%% @doc Special command. Executes Squery over pg_type table and updates codecs.
 -module(epgsql_cmd_update_type_cache).
 -module(epgsql_cmd_update_type_cache).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).

+ 5 - 2
src/datatypes/epgsql_codec_boolean.erl

@@ -1,8 +1,11 @@
 %%% @doc
 %%% @doc
 %%% Codec for `bool'.
 %%% Codec for `bool'.
+%%%
 %%% `unknown' is represented by `null'.
 %%% `unknown' is represented by `null'.
-%%% https://www.postgresql.org/docs/current/static/datatype-boolean.html
-%%% $PG$/src/backend/utils/adt/bool.c
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-boolean.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/bool.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 12 - 5
src/datatypes/epgsql_codec_bpchar.erl

@@ -1,9 +1,16 @@
 %%% @doc
 %%% @doc
-%%% Codec for `bpchar', `char' (CHAR(N), char).
-%%% ```SELECT 1::char''' ```SELECT 'abc'::char(10)'''
-%%% For 'text', 'varchar' see epgsql_codec_text.erl.
-%%% https://www.postgresql.org/docs/10/static/datatype-character.html
-%%% $PG$/src/backend/utils/adt/varchar.c
+%%% Codec for blank-padded fixed-size character type
+%%%
+%%% `CHAR' (single-byte) is represented as `byte()';
+%%% `CHARACTER(N) / CHAR(N)' as binary string
+%%%
+%%% <code>SELECT 1::char;</code> <code>SELECT 'abc'::char(10)</code>
+%%%
+%%% For 'text', 'varchar' see {@link epgsql_codec_text}.
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/10/static/datatype-character.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/varchar.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 11 - 4
src/datatypes/epgsql_codec_datetime.erl

@@ -1,9 +1,16 @@
 %%% @doc
 %%% @doc
 %%% Codec for `time', `timetz', `date', `timestamp', `timestamptz', `interval'
 %%% Codec for `time', `timetz', `date', `timestamp', `timestamptz', `interval'
-%%% https://www.postgresql.org/docs/current/static/datatype-datetime.html
-%%% $PG$/src/backend/utils/adt/timestamp.c // `timestamp', `timestamptz', `interval'
-%%% $PG$/src/backend/utils/adt/datetime.c // helpers
-%%% $PG$/src/backend/utils/adt/date.c // `time', `timetz', `date'
+%%%
+%%% It supports both integer and float datetime representations (see
+%%% [https://www.postgresql.org/docs/current/runtime-config-preset.html#GUC-INTEGER-DATETIMES]).
+%%% But float representation support might be eventually removed.
+%%%
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-datetime.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/timestamp.c // `timestamp', `timestamptz', `interval'</li>
+%%%  <li>$PG$/src/backend/utils/adt/datetime.c // helpers</li>
+%%%  <li>$PG$/src/backend/utils/adt/date.c // `time', `timetz', `date'</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 5 - 2
src/datatypes/epgsql_codec_float.erl

@@ -1,7 +1,10 @@
 %%% @doc
 %%% @doc
 %%% Codec for `float4', `float8' (real, double precision).
 %%% Codec for `float4', `float8' (real, double precision).
-%%% https://www.postgresql.org/docs/current/static/datatype-numeric.html#datatype-float
-%%% $PG$/src/backend/utils/adt/float.c
+%%%
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-numeric.html#datatype-float]</li>
+%%%  <li>$PG$/src/backend/utils/adt/float.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 9 - 3
src/datatypes/epgsql_codec_geometric.erl

@@ -1,8 +1,14 @@
 %%% @doc
 %%% @doc
 %%% Codec for `point'.
 %%% Codec for `point'.
-%%% https://www.postgresql.org/docs/current/static/datatype-geometric.html
-%%% $PG$/src/backend/utils/adt/geo_ops.c
-%%% XXX: it's not PostGIS!
+%%%
+%%% Codecs for other geometric datatypes (line, box, path, polygon, circle) can
+%%% be added later.
+%%%
+%%% XXX: it's not PostGIS! For PostGIS see {@link epgsql_codec_postgis}.
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-geometric.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/geo_ops.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% TODO: line, lseg, box, path, polygon, circle
 %%% TODO: line, lseg, box, path, polygon, circle

+ 65 - 32
src/datatypes/epgsql_codec_hstore.erl

@@ -1,10 +1,17 @@
 %%% @doc
 %%% @doc
 %%% Codec for `hstore' type.
 %%% Codec for `hstore' type.
-%%% https://www.postgresql.org/docs/current/static/hstore.html
-%%% XXX: hstore not a part of postgresql builtin datatypes, it's in contrib.
-%%% It should be enabled in postgresql by command
-%%% `CREATE EXTENSION hstore`
-%%% $PG$/contrib/hstore/
+%%%
+%%% Hstore codec can take a jiffy-style object or map() as input.
+%%% Output format can be changed by providing `return' option. See {@link return_format()}.
+%%% Values of hstore can be `NULL'. NULL representation can be changed by providing
+%%% `nulls' option, semantics is similar to {@link epgsql:connect_opts()} `nulls' option.
+%%%
+%%% XXX: hstore is not a part of postgresql builtin datatypes, it's in contrib.
+%%% It should be enabled in postgresql by command `CREATE EXTENSION hstore'.
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/hstore.html]</li>
+%%%  <li>$PG$/contrib/hstore/</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 
@@ -15,43 +22,71 @@
 
 
 -include("protocol.hrl").
 -include("protocol.hrl").
 
 
--export_type([data/0]).
+-export_type([data/0, options/0, return_format/0]).
 
 
 -type data() :: data_in() | data_out().
 -type data() :: data_in() | data_out().
 
 
 -type key_in() :: list() | binary() | atom() | integer() | float().
 -type key_in() :: list() | binary() | atom() | integer() | float().
-%% jiffy-style maps
--type data_in() :: { [{key_in(), binary()}] }.
--type data_out() :: { [{Key :: binary(), Value :: binary()}] }.
+-type data_in() :: { [{key_in(), binary()}] } |
+                   #{key_in() => binary() | atom()}.
+-type data_out() :: { [{Key :: binary(), Value :: binary()}] } |      % jiffy
+                    [{Key :: binary(), Value :: binary() | atom()}] | % proplist
+                    #{binary() => binary() | atom()}.                 % map
+
+-type return_format() :: map | jiffy | proplist.
+-type options() :: #{return => return_format(),
+                     nulls => [atom(), ...]}.
+
+-record(st,
+        {return :: return_format(),
+         nulls :: [atom(), ...]}).
 
 
 -dialyzer([{nowarn_function, [encode/3]}, no_improper_lists]).
 -dialyzer([{nowarn_function, [encode/3]}, no_improper_lists]).
 
 
-%% TODO: option for output format: proplist | jiffy-object | map
-init(_, _) -> [].
+init(Opts0, _) ->
+    Opts = epgsql:to_map(Opts0),
+    #st{return = maps:get(return, Opts, jiffy),
+        nulls = maps:get(nulls, Opts, [null, undefined])}.
 
 
 names() ->
 names() ->
     [hstore].
     [hstore].
 
 
-encode({Hstore}, hstore, _) when is_list(Hstore) ->
-    Size = length(Hstore),
-    %% TODO: construct improper list when support for Erl 17 will be dropped
-    Body = [[encode_key(K), encode_value(V)]
-           || {K, V} <- Hstore],
-    [<<Size:?int32>> | Body].
+encode({KV}, hstore, #st{nulls = Nulls}) when is_list(KV) ->
+    Size = length(KV),
+    encode_kv(KV, Size, Nulls);
+encode(Map, hstore, #st{nulls = Nulls}) when is_map(Map) ->
+    Size = map_size(Map),
+    encode_kv(maps:to_list(Map), Size, Nulls).
+
+decode(<<Size:?int32, Elements/binary>>, hstore, #st{return = Return, nulls = Nulls}) ->
+    KV = do_decode(Size, Elements, hd(Nulls)),
+    case Return of
+        jiffy ->
+            {KV};
+        map ->
+            maps:from_list(KV);
+        proplist ->
+            KV
+    end.
+
+decode_text(V, _, _) -> V.
 
 
-decode(<<Size:?int32, Elements/binary>>, hstore, _) ->
-    {do_decode(Size, Elements)}.
+%% Internal
 
 
+encode_kv(KV, Size, Nulls) ->
+    %% TODO: construct improper list when support for Erl 17 will be dropped
+    Body = [[encode_key(K), encode_value(V, Nulls)]
+           || {K, V} <- KV],
+    [<<Size:?int32>> | Body].
 
 
 encode_key(K) ->
 encode_key(K) ->
     encode_string(K).
     encode_string(K).
 
 
-encode_value(null) ->
-    <<-1:?int32>>;
-encode_value(undefined) ->
-    <<-1:?int32>>;
-encode_value(V) ->
-    encode_string(V).
+encode_value(V, Nulls) ->
+    case lists:member(V, Nulls) of
+        true -> <<-1:?int32>>;
+        false -> encode_string(V)
+    end.
 
 
 encode_string(Str) when is_binary(Str) ->
 encode_string(Str) when is_binary(Str) ->
     <<(byte_size(Str)):?int32, Str/binary>>;
     <<(byte_size(Str)):?int32, Str/binary>>;
@@ -66,11 +101,9 @@ encode_string(Str) when is_float(Str) ->
     %% encode_string(erlang:float_to_binary(Str)).
     %% encode_string(erlang:float_to_binary(Str)).
 
 
 
 
-do_decode(0, _) -> [];
-do_decode(N, <<KeyLen:?int32, Key:KeyLen/binary, -1:?int32, Rest/binary>>) ->
-    [{Key, null} | do_decode(N - 1, Rest)];
+do_decode(0, _, _) -> [];
+do_decode(N, <<KeyLen:?int32, Key:KeyLen/binary, -1:?int32, Rest/binary>>, Null) ->
+    [{Key, Null} | do_decode(N - 1, Rest, Null)];
 do_decode(N, <<KeyLen:?int32, Key:KeyLen/binary,
 do_decode(N, <<KeyLen:?int32, Key:KeyLen/binary,
-               ValLen:?int32, Value:ValLen/binary, Rest/binary>>) ->
-    [{Key, Value} | do_decode(N - 1, Rest)].
-
-decode_text(V, _, _) -> V.
+               ValLen:?int32, Value:ValLen/binary, Rest/binary>>, Null) ->
+    [{Key, Value} | do_decode(N - 1, Rest, Null)].

+ 29 - 4
src/datatypes/epgsql_codec_integer.erl

@@ -1,8 +1,11 @@
 %%% @doc
 %%% @doc
 %%% Codec for `int2', `int4', `int8' (smallint, integer, bigint).
 %%% Codec for `int2', `int4', `int8' (smallint, integer, bigint).
-%%% https://www.postgresql.org/docs/current/static/datatype-numeric.html#datatype-int
-%%% $PG$/src/backend/utils/adt/int.c
-%%% $PG$/src/backend/utils/adt/int8.c
+%%%
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-numeric.html#datatype-int]</li>
+%%%  <li>$PG$/src/backend/utils/adt/int.c</li>
+%%%  <li>$PG$/src/backend/utils/adt/int8.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 
@@ -10,16 +13,35 @@
 -behaviour(epgsql_codec).
 -behaviour(epgsql_codec).
 
 
 -export([init/2, names/0, encode/3, decode/3, decode_text/3]).
 -export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+-export([check_overflow_small/1, check_overflow_int/1, check_overflow_big/1]).
 
 
 -export_type([data/0]).
 -export_type([data/0]).
 
 
 %% See table 8.2
 %% See table 8.2
 %% https://www.postgresql.org/docs/current/static/datatype-numeric.html
 %% https://www.postgresql.org/docs/current/static/datatype-numeric.html
+-define(SMALLINT_MAX, 16#7fff).  % 32767, (2^15 - 1)
+-define(SMALLINT_MIN, -16#8000). % -32768
+-define(INT_MAX, 16#7fffffff).  % 2147483647, (2^31 - 1)
+-define(INT_MIN, -16#80000000). % -2147483648
 -define(BIGINT_MAX, 16#7fffffffffffffff).  % 9223372036854775807, (2^63 - 1)
 -define(BIGINT_MAX, 16#7fffffffffffffff).  % 9223372036854775807, (2^63 - 1)
--define(BIGINT_MIN, -16#7fffffffffffffff). % -9223372036854775807
+-define(BIGINT_MIN, -16#8000000000000000). % -9223372036854775808
 
 
 -type data() :: ?BIGINT_MIN..?BIGINT_MAX.
 -type data() :: ?BIGINT_MIN..?BIGINT_MAX.
 
 
+check_overflow_small(N) when N >= ?SMALLINT_MIN, N =< ?SMALLINT_MAX -> ok;
+check_overflow_small(N) ->
+    overflow(N, int2).
+
+check_overflow_int(N) when N >= ?INT_MIN, N =< ?INT_MAX -> ok;
+check_overflow_int(N) ->
+    overflow(N, int4).
+
+check_overflow_big(N) when N >= ?BIGINT_MIN, N =< ?BIGINT_MAX -> ok;
+check_overflow_big(N) ->
+    overflow(N, int8).
+
+overflow(N, Type) ->
+    error({integer_overflow, Type, N}).
 
 
 init(_, _) -> [].
 init(_, _) -> [].
 
 
@@ -27,10 +49,13 @@ names() ->
     [int2, int4, int8].
     [int2, int4, int8].
 
 
 encode(N, int2, _) ->
 encode(N, int2, _) ->
+    check_overflow_small(N),
     <<N:1/big-signed-unit:16>>;
     <<N:1/big-signed-unit:16>>;
 encode(N, int4, _) ->
 encode(N, int4, _) ->
+    check_overflow_int(N),
     <<N:1/big-signed-unit:32>>;
     <<N:1/big-signed-unit:32>>;
 encode(N, int8, _) ->
 encode(N, int8, _) ->
+    check_overflow_big(N),
     <<N:1/big-signed-unit:64>>.
     <<N:1/big-signed-unit:64>>.
 
 
 decode(<<N:1/big-signed-unit:16>>, int2, _)    -> N;
 decode(<<N:1/big-signed-unit:16>>, int2, _)    -> N;

+ 20 - 3
src/datatypes/epgsql_codec_intrange.erl

@@ -1,7 +1,12 @@
 %%% @doc
 %%% @doc
 %%% Codec for `int4range', `int8range' types.
 %%% Codec for `int4range', `int8range' types.
-%%% https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin
-%%% $PG$/src/backend/utils/adt/rangetypes.c
+%%%
+%%% <ul>
+%%%   <li>[https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin]</li>
+%%%   <li>$PG$/src/backend/utils/adt/rangetypes.c</li>
+%%% </ul>
+%%% @end
+%%% @see epgsql_codec_integer
 %%% @end
 %%% @end
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% TODO: universal range, based on pg_range table
 %%% TODO: universal range, based on pg_range table
@@ -16,7 +21,7 @@
 
 
 -export_type([data/0]).
 -export_type([data/0]).
 
 
--type data() :: {left(), right()}.
+-type data() :: {left(), right()} | empty.
 
 
 -type left() :: minus_infinity | integer().
 -type left() :: minus_infinity | integer().
 -type right() :: plus_infinity | integer().
 -type right() :: plus_infinity | integer().
@@ -27,11 +32,15 @@ init(_, _) -> [].
 names() ->
 names() ->
     [int4range, int8range].
     [int4range, int8range].
 
 
+encode(empty, _, _) ->
+    <<1>>;
 encode(Range, int4range, _) ->
 encode(Range, int4range, _) ->
     encode_int4range(Range);
     encode_int4range(Range);
 encode(Range, int8range, _) ->
 encode(Range, int8range, _) ->
     encode_int8range(Range).
     encode_int8range(Range).
 
 
+decode(<<1>>, _, _) ->
+    empty;
 decode(Bin, int4range, _) ->
 decode(Bin, int4range, _) ->
     decode_int4range(Bin);
     decode_int4range(Bin);
 decode(Bin, int8range, _) ->
 decode(Bin, int8range, _) ->
@@ -42,26 +51,34 @@ encode_int4range({minus_infinity, plus_infinity}) ->
     <<24:1/big-signed-unit:8>>;
     <<24:1/big-signed-unit:8>>;
 encode_int4range({From, plus_infinity}) ->
 encode_int4range({From, plus_infinity}) ->
     FromInt = to_int(From),
     FromInt = to_int(From),
+    epgsql_codec_integer:check_overflow_int(FromInt),
     <<18:1/big-signed-unit:8, 4:?int32, FromInt:?int32>>;
     <<18:1/big-signed-unit:8, 4:?int32, FromInt:?int32>>;
 encode_int4range({minus_infinity, To}) ->
 encode_int4range({minus_infinity, To}) ->
     ToInt = to_int(To),
     ToInt = to_int(To),
+    epgsql_codec_integer:check_overflow_int(ToInt),
     <<8:1/big-signed-unit:8, 4:?int32, ToInt:?int32>>;
     <<8:1/big-signed-unit:8, 4:?int32, ToInt:?int32>>;
 encode_int4range({From, To}) ->
 encode_int4range({From, To}) ->
     FromInt = to_int(From),
     FromInt = to_int(From),
     ToInt = to_int(To),
     ToInt = to_int(To),
+    epgsql_codec_integer:check_overflow_int(FromInt),
+    epgsql_codec_integer:check_overflow_int(ToInt),
     <<2:1/big-signed-unit:8, 4:?int32, FromInt:?int32, 4:?int32, ToInt:?int32>>.
     <<2:1/big-signed-unit:8, 4:?int32, FromInt:?int32, 4:?int32, ToInt:?int32>>.
 
 
 encode_int8range({minus_infinity, plus_infinity}) ->
 encode_int8range({minus_infinity, plus_infinity}) ->
     <<24:1/big-signed-unit:8>>;
     <<24:1/big-signed-unit:8>>;
 encode_int8range({From, plus_infinity}) ->
 encode_int8range({From, plus_infinity}) ->
     FromInt = to_int(From),
     FromInt = to_int(From),
+    epgsql_codec_integer:check_overflow_big(FromInt),
     <<18:1/big-signed-unit:8, 8:?int32, FromInt:?int64>>;
     <<18:1/big-signed-unit:8, 8:?int32, FromInt:?int64>>;
 encode_int8range({minus_infinity, To}) ->
 encode_int8range({minus_infinity, To}) ->
     ToInt = to_int(To),
     ToInt = to_int(To),
+    epgsql_codec_integer:check_overflow_big(ToInt),
     <<8:1/big-signed-unit:8, 8:?int32, ToInt:?int64>>;
     <<8:1/big-signed-unit:8, 8:?int32, ToInt:?int64>>;
 encode_int8range({From, To}) ->
 encode_int8range({From, To}) ->
     FromInt = to_int(From),
     FromInt = to_int(From),
     ToInt = to_int(To),
     ToInt = to_int(To),
+    epgsql_codec_integer:check_overflow_big(FromInt),
+    epgsql_codec_integer:check_overflow_big(ToInt),
     <<2:1/big-signed-unit:8, 8:?int32, FromInt:?int64, 8:?int32, ToInt:?int64>>.
     <<2:1/big-signed-unit:8, 8:?int32, FromInt:?int64, 8:?int32, ToInt:?int64>>.
 
 
 to_int(N) when is_integer(N) -> N;
 to_int(N) when is_integer(N) -> N;

+ 8 - 3
src/datatypes/epgsql_codec_json.erl

@@ -1,8 +1,13 @@
 %%% @doc
 %%% @doc
 %%% Codec for `json', `jsonb'
 %%% Codec for `json', `jsonb'
-%%% https://www.postgresql.org/docs/current/static/datatype-json.html
-%%% $PG$/src/backend/utils/adt/json.c // `json'
-%%% $PG$/src/backend/utils/adt/jsonb.c // `jsonb'
+%%%
+%%% It is possible to instruct the codec to do JSON encoding/decoding to Erlang
+%%% terms by providing callback module name, see {@link json_mod()}.
+%%% <ul>
+%%%   <li>[https://www.postgresql.org/docs/current/static/datatype-json.html]</li>
+%%%   <li>$PG$/src/backend/utils/adt/json.c // `json'</li>
+%%%   <li>$PG$/src/backend/utils/adt/jsonb.c // `jsonb'</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 6 - 3
src/datatypes/epgsql_codec_net.erl

@@ -1,9 +1,12 @@
 %%% @doc
 %%% @doc
 %%% Codec for `inet', `cidr'
 %%% Codec for `inet', `cidr'
-%%% https://www.postgresql.org/docs/10/static/datatype-net-types.html
-%%% $PG$/src/backend/utils/adt/network.c
 %%%
 %%%
-%%% TIP: use `inet:ntoa/1' to convert `ip()' to string.
+%%% TIP: use {@link inet:ntoa/1} and {@link inet:parse_address/1} to convert
+%%% between {@link ip()} and `string()'.
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/10/static/datatype-net-types.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/network.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 -module(epgsql_codec_net).
 -module(epgsql_codec_net).

+ 1 - 0
src/datatypes/epgsql_codec_noop.erl

@@ -1,3 +1,4 @@
+%%% @private
 %%% @doc
 %%% @doc
 %%% Dummy codec. Used internally
 %%% Dummy codec. Used internally
 %%% @end
 %%% @end

+ 9 - 2
src/datatypes/epgsql_codec_postgis.erl

@@ -1,7 +1,14 @@
 %%% @doc
 %%% @doc
 %%% Codec for `geometry' PostGIS umbrella datatype.
 %%% Codec for `geometry' PostGIS umbrella datatype.
-%%% http://postgis.net/docs/manual-2.4/geometry.html
-%%% $POSTGIS$/postgis/lwgeom_inout.c
+%%%
+%%% XXX: PostGIS is not a Postgres's built-in datatype! It should be instaled
+%%% separately and enabled via `CREATE EXTENSION postgis'.
+%%% <ul>
+%%%  <li>[http://postgis.net/docs/manual-2.4/geometry.html]</li>
+%%%  <li>$POSTGIS$/postgis/lwgeom_inout.c</li>
+%%% </ul>
+%%% @end
+%%% @see ewkb
 %%% @end
 %%% @end
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 13 - 4
src/datatypes/epgsql_codec_text.erl

@@ -1,9 +1,18 @@
 %%% @doc
 %%% @doc
 %%% Codec for `text', `varchar', `bytea'.
 %%% Codec for `text', `varchar', `bytea'.
-%%% For 'char' see epgsql_codec_bpchar.erl.
-%%% https://www.postgresql.org/docs/10/static/datatype-character.html
-%%% $PG$/src/backend/utils/adt/varchar.c
-%%% $PG$/src/backend/utils/adt/varlena.c
+%%%
+%%% If input for `text' or `varchar' is provided as a list, not binary, and it
+%%% contains not just `byte()', an attempt to perform unicode conversion will be made.
+%%%
+%%% Also, `integer()', `float()' and `atom()' are automatically converted to
+%%% strings, but this kind of conversion might be eventualy removed.
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/10/static/datatype-character.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/varchar.c</li>
+%%%  <li>$PG$/src/backend/utils/adt/varlena.c</li>
+%%% </ul>
+%%% @end
+%%% @see epgsql_codec_bpchar. epgsql_codec_bpchar - for 'char' and 'char(N)'
 %%% @end
 %%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 30 - 6
src/datatypes/epgsql_codec_timerange.erl

@@ -1,7 +1,12 @@
 %%% @doc
 %%% @doc
 %%% Codec for `tsrange', `tstzrange', `daterange' types.
 %%% Codec for `tsrange', `tstzrange', `daterange' types.
-%%% https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin
-%%% $PG$/src/backend/utils/adt/rangetypes.c
+%%%
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin]</li>
+%%%  <li>$PG$/src/backend/utils/adt/rangetypes.c</li>
+%%% </ul>
+%%% @end
+%%% @see epgsql_codec_datetime
 %%% @end
 %%% @end
 %%% Created : 16 Jul 2018 by Vladimir Sekissov <eryx67@gmail.com>
 %%% Created : 16 Jul 2018 by Vladimir Sekissov <eryx67@gmail.com>
 %%% TODO: universal range, based on pg_range table
 %%% TODO: universal range, based on pg_range table
@@ -16,7 +21,10 @@
 
 
 -export_type([data/0]).
 -export_type([data/0]).
 
 
--type data() :: {epgsql_codec_datetime:data(), epgsql_codec_datetime:data()} | empty.
+-type data() :: {left(), right()} | empty.
+
+-type left() :: minus_infinity | epgsql_codec_datetime:data().
+-type right() :: plus_infinity | epgsql_codec_datetime:data().
 
 
 init(_, Sock) ->
 init(_, Sock) ->
     case epgsql_sock:get_parameter_internal(<<"integer_datetimes">>, Sock) of
     case epgsql_sock:get_parameter_internal(<<"integer_datetimes">>, Sock) of
@@ -29,6 +37,14 @@ names() ->
 
 
 encode(empty, _T, _CM) ->
 encode(empty, _T, _CM) ->
     <<1>>;
     <<1>>;
+encode({minus_infinity, plus_infinity}, _T, _CM) ->
+    <<24:1/big-signed-unit:8>>;
+encode({From, plus_infinity}, Type, EncMod) ->
+    FromBin = encode_member(Type, From, EncMod),
+    <<18:1/big-signed-unit:8, (byte_size(FromBin)):?int32, FromBin/binary>>;
+encode({minus_infinity, To}, Type, EncMod) ->
+    ToBin = encode_member(Type, To, EncMod),
+    <<8:1/big-signed-unit:8, (byte_size(ToBin)):?int32, ToBin/binary>>;
 encode({From, To}, Type, EncMod) ->
 encode({From, To}, Type, EncMod) ->
     FromBin = encode_member(Type, From, EncMod),
     FromBin = encode_member(Type, From, EncMod),
     ToBin = encode_member(Type, To, EncMod),
     ToBin = encode_member(Type, To, EncMod),
@@ -38,11 +54,19 @@ encode({From, To}, Type, EncMod) ->
 
 
 decode(<<1>>, _, _) ->
 decode(<<1>>, _, _) ->
     empty;
     empty;
-decode(<<2:1/big-signed-unit:8,
+decode(<<Flag:1/big-signed-unit:8,
          FromLen:?int32, FromBin:FromLen/binary,
          FromLen:?int32, FromBin:FromLen/binary,
          ToLen:?int32, ToBin:ToLen/binary>>,
          ToLen:?int32, ToBin:ToLen/binary>>,
-       Type, EncMod) ->
-    {decode_member(Type, FromBin, EncMod), decode_member(Type, ToBin, EncMod)}.
+       Type, EncMod) when Flag =:= 0; Flag =:= 2; Flag =:= 4; Flag =:= 6 -> %% () [) (] []
+    {decode_member(Type, FromBin, EncMod), decode_member(Type, ToBin, EncMod)};
+decode(<<Flag:1/big-signed-unit:8, ToLen:?int32, ToBin:ToLen/binary>>,
+    Type, EncMod) when Flag =:= 8; Flag =:= 12 -> %% (] ()
+    {minus_infinity, decode_member(Type, ToBin, EncMod)};
+decode(<<Flag:1/big-signed-unit:8, FromLen:?int32, FromBin:FromLen/binary>>,
+    Type, EncMod) when Flag =:= 16; Flag =:= 18 -> %% [) ()
+    {decode_member(Type, FromBin, EncMod), plus_infinity};
+decode(<<24:1/big-signed-unit:8>>, _, _) ->
+    {minus_infinity, plus_infinity}.
 
 
 decode_text(V, _, _) -> V.
 decode_text(V, _, _) -> V.
 
 

+ 6 - 3
src/datatypes/epgsql_codec_uuid.erl

@@ -1,9 +1,12 @@
 %%% @doc
 %%% @doc
 %%% Codec for `uuid' type.
 %%% Codec for `uuid' type.
-%%% Input expected to be in hex string, eg
+%%%
+%%% Input is expected to be in hex `string()' / `binary()', eg
 %%% `<<"550e8400-e29b-41d4-a716-446655440000">>'.
 %%% `<<"550e8400-e29b-41d4-a716-446655440000">>'.
-%%% https://www.postgresql.org/docs/current/static/datatype-uuid.html
-%%% $PG$/src/backend/utils/adt/uuid.c
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/datatype-uuid.html]</li>
+%%%  <li>$PG$/src/backend/utils/adt/uuid.c</li>
+%%% </ul>
 %%% @end
 %%% @end
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 

+ 1 - 1
src/epgsql.app.src

@@ -1,6 +1,6 @@
 {application, epgsql,
 {application, epgsql,
  [{description, "PostgreSQL Client"},
  [{description, "PostgreSQL Client"},
-  {vsn, "4.3.0"},
+  {vsn, "4.4.0"},
   {modules, []},
   {modules, []},
   {registered, []},
   {registered, []},
   {applications, [kernel,
   {applications, [kernel,

+ 82 - 49
src/epgsql.erl

@@ -1,3 +1,7 @@
+%%% @doc Synchronous interface.
+%%%
+%%% All functions block (with infinite timeout) until full result is available.
+%%% @end
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
 
@@ -15,7 +19,7 @@
          describe/2, describe/3,
          describe/2, describe/3,
          bind/3, bind/4,
          bind/3, bind/4,
          execute/2, execute/3, execute/4,
          execute/2, execute/3, execute/4,
-         execute_batch/2,
+         execute_batch/2, execute_batch/3,
          close/2, close/3,
          close/2, close/3,
          sync/1,
          sync/1,
          cancel/1,
          cancel/1,
@@ -42,7 +46,7 @@
 
 
 -include("epgsql.hrl").
 -include("epgsql.hrl").
 
 
--type sql_query() :: iodata().
+-type sql_query() :: iodata(). % SQL query text
 -type host() :: inet:ip_address() | inet:hostname().
 -type host() :: inet:ip_address() | inet:hostname().
 -type password() :: string() | iodata() | fun( () -> iodata() ).
 -type password() :: string() | iodata() | fun( () -> iodata() ).
 -type connection() :: pid().
 -type connection() :: pid().
@@ -57,6 +61,7 @@
     {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
     {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
     {async,    Receiver   :: pid() | atom()}       | % process to receive LISTEN/NOTIFY msgs
     {async,    Receiver   :: pid() | atom()}       | % process to receive LISTEN/NOTIFY msgs
     {codecs,   Codecs     :: [{epgsql_codec:codec_mod(), any()}]} |
     {codecs,   Codecs     :: [{epgsql_codec:codec_mod(), any()}]} |
+    {nulls,    Nulls      :: [any(), ...]} |    % terms to be used as NULL
     {replication, Replication :: string()}. % Pass "database" to connect in replication mode
     {replication, Replication :: string()}. % Pass "database" to connect in replication mode
 
 
 -type connect_opts() ::
 -type connect_opts() ::
@@ -71,10 +76,11 @@
           timeout => timeout(),
           timeout => timeout(),
           async => pid() | atom(),
           async => pid() | atom(),
           codecs => [{epgsql_codec:codec_mod(), any()}],
           codecs => [{epgsql_codec:codec_mod(), any()}],
+          nulls => [any(), ...],
           replication => string()}.
           replication => string()}.
 
 
 -type connect_error() :: epgsql_cmd_connect:connect_error().
 -type connect_error() :: epgsql_cmd_connect:connect_error().
--type query_error() :: #error{}.
+-type query_error() :: #error{}.              % Error report generated by server
 
 
 
 
 -type type_name() :: atom().
 -type type_name() :: atom().
@@ -88,15 +94,15 @@
 -type pg_datetime() :: epgsql_codec_datetime:pg_datetime().
 -type pg_datetime() :: epgsql_codec_datetime:pg_datetime().
 -type pg_interval() :: epgsql_codec_datetime:pg_interval().
 -type pg_interval() :: epgsql_codec_datetime:pg_interval().
 
 
-%% Deprecated
 -type bind_param() :: any().
 -type bind_param() :: any().
+%% Value to be bound to placeholder (`$1', `$2' etc)
 
 
 -type typed_param() :: {epgsql_type(), bind_param()}.
 -type typed_param() :: {epgsql_type(), bind_param()}.
 
 
 -type column() :: #column{}.
 -type column() :: #column{}.
 -type statement() :: #statement{}.
 -type statement() :: #statement{}.
 -type squery_row() :: tuple(). % tuple of binary().
 -type squery_row() :: tuple(). % tuple of binary().
--type equery_row() :: tuple(). % tuple of bind_param().
+-type equery_row() :: tuple(). % tuple of any().
 -type ok_reply(RowType) ::
 -type ok_reply(RowType) ::
         %% select
         %% select
     {ok, ColumnsDescription :: [column()], RowsValues :: [RowType]} |
     {ok, ColumnsDescription :: [column()], RowsValues :: [RowType]} |
@@ -124,6 +130,7 @@
 %% -------------
 %% -------------
 
 
 %% -- client interface --
 %% -- client interface --
+%% @doc connects to the server and performs all the necessary handshakes
 -spec connect(connect_opts())
 -spec connect(connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
 connect(Opts) ->
 connect(Opts) ->
@@ -138,13 +145,13 @@ connect(Host, Username, Opts) ->
 
 
 -spec connect(host(), string(), password(), connect_opts())
 -spec connect(host(), string(), password(), connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-%% @doc connects to Postgres
-%% where
-%% `Host'     - host to connect to
-%% `Username' - username to connect as, defaults to `$USER'
-%% `Password' - optional password to authenticate with
-%% `Opts'     - proplist or map of extra options
-%% returns `{ok, Connection}' otherwise `{error, Reason}'
+%% @doc connects to the server and performs all the necessary handshakes (legacy interface)
+%% @param Host     host to connect to
+%% @param Username username to connect as, defaults to `$USER'
+%% @param Password optional password to authenticate with
+%% @param Opts     proplist or map of extra options
+%% @returns `{ok, Connection}' otherwise `{error, Reason}'
+%% @see connect/1
 connect(Host, Username, Password, Opts) ->
 connect(Host, Username, Password, Opts) ->
     {ok, C} = epgsql_sock:start_link(),
     {ok, C} = epgsql_sock:start_link(),
     connect(C, Host, Username, Password, Opts).
     connect(C, Host, Username, Password, Opts).
@@ -214,38 +221,37 @@ get_parameter(C, Name) ->
 set_notice_receiver(C, PidOrName) ->
 set_notice_receiver(C, PidOrName) ->
     epgsql_sock:set_notice_receiver(C, PidOrName).
     epgsql_sock:set_notice_receiver(C, PidOrName).
 
 
-%% @doc Returns last command status message
-%% If multiple queries were executed using `squery/2', separated by semicolon,
+%% @doc Returns last command status message.
+%% If multiple queries were executed using {@link squery/2}, separated by semicolon,
 %% only the last query's status will be available.
 %% only the last query's status will be available.
-%% See https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQCMDSTATUS
+%% See [https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQCMDSTATUS]
 -spec get_cmd_status(connection()) -> {ok, Status}
 -spec get_cmd_status(connection()) -> {ok, Status}
                                           when
                                           when
       Status :: undefined | atom() | {atom(), integer()}.
       Status :: undefined | atom() | {atom(), integer()}.
 get_cmd_status(C) ->
 get_cmd_status(C) ->
     epgsql_sock:get_cmd_status(C).
     epgsql_sock:get_cmd_status(C).
 
 
--spec squery(connection(), sql_query()) -> epgsql_cmd_squery:response().
+-spec squery(connection(), sql_query()) -> epgsql_cmd_squery:response() | epgsql_sock:error().
 %% @doc runs simple `SqlQuery' via given `Connection'
 %% @doc runs simple `SqlQuery' via given `Connection'
+%% @see epgsql_cmd_squery
 squery(Connection, SqlQuery) ->
 squery(Connection, SqlQuery) ->
     epgsql_sock:sync_command(Connection, epgsql_cmd_squery, SqlQuery).
     epgsql_sock:sync_command(Connection, epgsql_cmd_squery, SqlQuery).
 
 
 equery(C, Sql) ->
 equery(C, Sql) ->
     equery(C, Sql, []).
     equery(C, Sql, []).
 
 
-%% TODO add fast_equery command that doesn't need parsed statement
 -spec equery(connection(), sql_query(), [bind_param()]) ->
 -spec equery(connection(), sql_query(), [bind_param()]) ->
-                    epgsql_cmd_equery:response().
+                    epgsql_cmd_equery:response() | epgsql_sock:error().
 equery(C, Sql, Parameters) ->
 equery(C, Sql, Parameters) ->
-    case parse(C, "", Sql, []) of
-        {ok, #statement{types = Types} = S} ->
-            TypedParameters = lists:zip(Types, Parameters),
-            epgsql_sock:sync_command(C, epgsql_cmd_equery, {S, TypedParameters});
-        Error ->
-            Error
-    end.
+    equery(C, "", Sql, Parameters).
 
 
+%% @doc Executes extended query
+%% @end
+%% @see epgsql_cmd_equery
+%% @end
+%% TODO add fast_equery command that doesn't need parsed statement
 -spec equery(connection(), string(), sql_query(), [bind_param()]) ->
 -spec equery(connection(), string(), sql_query(), [bind_param()]) ->
-                    epgsql_cmd_equery:response().
+                    epgsql_cmd_equery:response() | epgsql_sock:error().
 equery(C, Name, Sql, Parameters) ->
 equery(C, Name, Sql, Parameters) ->
     case parse(C, Name, Sql, []) of
     case parse(C, Name, Sql, []) of
         {ok, #statement{types = Types} = S} ->
         {ok, #statement{types = Types} = S} ->
@@ -255,13 +261,17 @@ equery(C, Name, Sql, Parameters) ->
             Error
             Error
     end.
     end.
 
 
--spec prepared_query(C::connection(), Name::string(), Parameters::[bind_param()]) ->
+%% @doc Similar to {@link equery/3}, but uses prepared statement that can be reused multiple times.
+%% @see epgsql_cmd_prepared_query
+-spec prepared_query(C::connection(), string() | statement(), Parameters::[bind_param()]) ->
                             epgsql_cmd_prepared_query:response().
                             epgsql_cmd_prepared_query:response().
-prepared_query(C, Name, Parameters) ->
+prepared_query(C, #statement{types = Types} = S, Parameters) ->
+    TypedParameters = lists:zip(Types, Parameters),
+    epgsql_sock:sync_command(C, epgsql_cmd_prepared_query, {S, TypedParameters});
+prepared_query(C, Name, Parameters) when is_list(Name) ->
     case describe(C, statement, Name) of
     case describe(C, statement, Name) of
-        {ok, #statement{types = Types} = S} ->
-            TypedParameters = lists:zip(Types, Parameters),
-            epgsql_sock:sync_command(C, epgsql_cmd_prepared_query, {S, TypedParameters});
+        {ok, #statement{} = S} ->
+            prepared_query(C, S, Parameters);
         Error ->
         Error ->
             Error
             Error
     end.
     end.
@@ -308,11 +318,28 @@ execute(C, S, N) ->
 execute(C, S, PortalName, N) ->
 execute(C, S, PortalName, N) ->
     epgsql_sock:sync_command(C, epgsql_cmd_execute, {S, PortalName, N}).
     epgsql_sock:sync_command(C, epgsql_cmd_execute, {S, PortalName, N}).
 
 
+%% @doc Executes batch of `{statement(), [bind_param()]}' extended queries
+%% @see epgsql_cmd_batch
 -spec execute_batch(connection(), [{statement(), [bind_param()]}]) ->
 -spec execute_batch(connection(), [{statement(), [bind_param()]}]) ->
                            epgsql_cmd_batch:response().
                            epgsql_cmd_batch:response().
 execute_batch(C, Batch) ->
 execute_batch(C, Batch) ->
     epgsql_sock:sync_command(C, epgsql_cmd_batch, Batch).
     epgsql_sock:sync_command(C, epgsql_cmd_batch, Batch).
 
 
+%% @doc Executes same statement() extended query with each parameter list of a `Batch'
+%% @see epgsql_cmd_batch
+-spec execute_batch(connection(), statement() | sql_query(), [ [bind_param()] ]) ->
+                           {[column()], epgsql_cmd_batch:response()}.
+execute_batch(C, #statement{columns = Cols} = Statement, Batch) ->
+    {Cols, epgsql_sock:sync_command(C, epgsql_cmd_batch, {Statement, Batch})};
+execute_batch(C, Sql, Batch) ->
+    case parse(C, Sql) of
+        {ok, #statement{} = S} ->
+            execute_batch(C, S, Batch);
+        Error ->
+            Error
+    end.
+
+
 %% statement/portal functions
 %% statement/portal functions
 -spec describe(connection(), statement()) -> epgsql_cmd_describe_statement:response().
 -spec describe(connection(), statement()) -> epgsql_cmd_describe_statement:response().
 describe(C, #statement{name = Name}) ->
 describe(C, #statement{name = Name}) ->
@@ -335,6 +362,7 @@ describe(C, portal, Name) ->
 close(C, #statement{name = Name}) ->
 close(C, #statement{name = Name}) ->
     close(C, statement, Name).
     close(C, statement, Name).
 
 
+%% @doc close statement or portal
 -spec close(connection(), statement | portal, iodata()) -> epgsql_cmd_close:response().
 -spec close(connection(), statement | portal, iodata()) -> epgsql_cmd_close:response().
 close(C, Type, Name) ->
 close(C, Type, Name) ->
     epgsql_sock:sync_command(C, epgsql_cmd_close, {Type, Name}).
     epgsql_sock:sync_command(C, epgsql_cmd_close, {Type, Name}).
@@ -343,6 +371,7 @@ close(C, Type, Name) ->
 sync(C) ->
 sync(C) ->
     epgsql_sock:sync_command(C, epgsql_cmd_sync, []).
     epgsql_sock:sync_command(C, epgsql_cmd_sync, []).
 
 
+%% @doc cancel currently executing command
 -spec cancel(connection()) -> ok.
 -spec cancel(connection()) -> ok.
 cancel(C) ->
 cancel(C) ->
     epgsql_sock:cancel(C).
     epgsql_sock:cancel(C).
@@ -358,15 +387,20 @@ with_transaction(C, F) ->
 %% @doc Execute callback function with connection in a transaction.
 %% @doc Execute callback function with connection in a transaction.
 %% Transaction will be rolled back in case of exception.
 %% Transaction will be rolled back in case of exception.
 %% Options (proplist or map):
 %% Options (proplist or map):
-%% - reraise (true): when set to true, exception will be re-thrown, otherwise
-%%   {rollback, ErrorReason} will be returned
-%% - ensure_comitted (false): even when callback returns without exception,
+%% <dl>
+%%  <dt>reraise</dt>
+%%  <dd>when set to true, exception will be re-thrown, otherwise
+%%   `{rollback, ErrorReason}' will be returned. Default: `true'</dd>
+%%  <dt>ensure_comitted</dt>
+%%  <dd>even when callback returns without exception,
 %%   check that transaction was comitted by checking CommandComplete status
 %%   check that transaction was comitted by checking CommandComplete status
 %%   of "COMMIT" command. In case when transaction was rolled back, status will be
 %%   of "COMMIT" command. In case when transaction was rolled back, status will be
-%%   "rollback" instead of "commit".
-%% - begin_opts (""): append extra options to "BEGIN" command (see
+%%   "rollback" instead of "commit". Default: `false'</dd>
+%%  <dt>begin_opts</dt>
+%%  <dd>append extra options to "BEGIN" command (see
 %%   https://www.postgresql.org/docs/current/static/sql-begin.html)
 %%   https://www.postgresql.org/docs/current/static/sql-begin.html)
-%%   Beware of SQL injections! No escaping is made on begin_opts!
+%%   Beware of SQL injections! No escaping is made on begin_opts! Default: `""'</dd>
+%% </dl>
 -spec with_transaction(
 -spec with_transaction(
         connection(), fun((connection()) -> Reply), Opts) -> Reply | {rollback, any()} | no_return() when
         connection(), fun((connection()) -> Reply), Opts) -> Reply | {rollback, any()} | no_return() when
       Reply :: any(),
       Reply :: any(),
@@ -431,18 +465,17 @@ handle_x_log_data(Mod, StartLSN, EndLSN, WALRecord, Repl) ->
     Response :: epgsql_cmd_start_replication:response(),
     Response :: epgsql_cmd_start_replication:response(),
     Callback :: module() | pid().
     Callback :: module() | pid().
 %% @doc instructs Postgres server to start streaming WAL for logical replication
 %% @doc instructs Postgres server to start streaming WAL for logical replication
-%% where
-%% `Connection'      - connection in replication mode
-%% `ReplicationSlot' - the name of the replication slot to stream changes from
-%% `Callback'        - Callback module which should have the callback functions implemented for message processing.
-%%                      or a process which should be able to receive replication messages.
-%% `CbInitState'     - Callback Module's initial state
-%% `WALPosition'     - the WAL position XXX/XXX to begin streaming at.
-%%                      "0/0" to let the server determine the start point.
-%% `PluginOpts'      - optional options passed to the slot's logical decoding plugin.
-%%                      For example: "option_name1 'value1', option_name2 'value2'"
-%% `Opts'            - options of logical replication
-%% returns `ok' otherwise `{error, Reason}'
+%% @param Connection      connection in replication mode
+%% @param ReplicationSlot the name of the replication slot to stream changes from
+%% @param Callback        Callback module which should have the callback functions implemented for message processing.
+%%                        or a process which should be able to receive replication messages.
+%% @param CbInitState     Callback Module's initial state
+%% @param WALPosition     the WAL position XXX/XXX to begin streaming at.
+%%                        "0/0" to let the server determine the start point.
+%% @param PluginOpts      optional options passed to the slot's logical decoding plugin.
+%%                        For example: "option_name1 'value1', option_name2 'value2'"
+%% @param Opts            options of logical replication
+%% @returns `ok' otherwise `{error, Reason}'
 start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts) ->
 start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts) ->
     Command = {ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, to_map(Opts)},
     Command = {ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, to_map(Opts)},
     epgsql_sock:sync_command(Connection, epgsql_cmd_start_replication, Command).
     epgsql_sock:sync_command(Connection, epgsql_cmd_start_replication, Command).

+ 118 - 51
src/epgsql_binary.erl

@@ -1,9 +1,17 @@
+%%% @doc
+%%% Interface to encoder/decoder for binary postgres data representation
+%%% @end
+%%% @see epgsql_codec
+%%% @see epgsql_wire
+%%% @end
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
-%% XXX: maybe merge this module into epgsql_codec?
+%%% XXX: maybe merge this module into epgsql_codec?
 -module(epgsql_binary).
 -module(epgsql_binary).
 
 
 -export([new_codec/2,
 -export([new_codec/2,
          update_codec/2,
          update_codec/2,
+         null/1,
+         is_null/2,
          type_to_oid/2,
          type_to_oid/2,
          typeinfo_to_name_array/2,
          typeinfo_to_name_array/2,
          typeinfo_to_oid_info/2,
          typeinfo_to_oid_info/2,
@@ -17,10 +25,24 @@
 -export_type([codec/0, decoder/0]).
 -export_type([codec/0, decoder/0]).
 
 
 -include("protocol.hrl").
 -include("protocol.hrl").
+-define(DEFAULT_NULLS, [null, undefined]).
 
 
 -record(codec,
 -record(codec,
-        {opts = [] :: list(),                   % not used yet
+        {opts = #{} :: opts(),                   % not used yet
+         nulls = ?DEFAULT_NULLS :: nulls(),
          oid_db :: epgsql_oid_db:db()}).
          oid_db :: epgsql_oid_db:db()}).
+-record(array_decoder,
+        {element_decoder :: decoder(),
+         null_term :: any() }).
+-record(array_encoder,
+        {element_encoder :: epgsql_codec:codec_entry(),
+         n_dims = 0 :: non_neg_integer(),
+         lengths = [] :: [non_neg_integer()],
+         has_null = false :: boolean(),
+         codec :: codec()}).
+
+-type nulls() :: [any(), ...].
+-type opts() :: #{nulls => nulls()}.
 
 
 -opaque codec() :: #codec{}.
 -opaque codec() :: #codec{}.
 -opaque decoder() :: {fun((binary(), epgsql:type_name(), epgsql_codec:codec_state()) -> any()),
 -opaque decoder() :: {fun((binary(), epgsql:type_name(), epgsql_codec:codec_state()) -> any()),
@@ -36,7 +58,7 @@
 %% Codec is used to convert data (result rows and query parameters) between Erlang and postgresql formats
 %% Codec is used to convert data (result rows and query parameters) between Erlang and postgresql formats
 %% It uses mappings between OID, type names and `epgsql_codec_*' modules (epgsql_oid_db)
 %% It uses mappings between OID, type names and `epgsql_codec_*' modules (epgsql_oid_db)
 
 
--spec new_codec(epgsql_sock:pg_sock(), list()) -> codec().
+-spec new_codec(epgsql_sock:pg_sock(), opts()) -> codec().
 new_codec(PgSock, Opts) ->
 new_codec(PgSock, Opts) ->
     Codecs = default_codecs(),
     Codecs = default_codecs(),
     Oids = default_oids(),
     Oids = default_oids(),
@@ -45,7 +67,9 @@ new_codec(PgSock, Opts) ->
 new_codec(PgSock, Codecs, Oids, Opts) ->
 new_codec(PgSock, Codecs, Oids, Opts) ->
     CodecEntries = epgsql_codec:init_mods(Codecs, PgSock),
     CodecEntries = epgsql_codec:init_mods(Codecs, PgSock),
     Types = epgsql_oid_db:join_codecs_oids(Oids, CodecEntries),
     Types = epgsql_oid_db:join_codecs_oids(Oids, CodecEntries),
-    #codec{oid_db = epgsql_oid_db:from_list(Types), opts = Opts}.
+    #codec{oid_db = epgsql_oid_db:from_list(Types),
+           nulls = maps:get(nulls, Opts, ?DEFAULT_NULLS),
+           opts = Opts}.
 
 
 -spec update_codec([epgsql_oid_db:type_info()], codec()) -> codec().
 -spec update_codec([epgsql_oid_db:type_info()], codec()) -> codec().
 update_codec(TypeInfos, #codec{oid_db = Db} = Codec) ->
 update_codec(TypeInfos, #codec{oid_db = Db} = Codec) ->
@@ -63,6 +87,16 @@ oid_to_name(Oid, Codec) ->
             end
             end
     end.
     end.
 
 
+%% @doc Return the value that represents NULL (1st element of `nulls' list)
+-spec null(codec()) -> any().
+null(#codec{nulls = [Null | _]}) ->
+    Null.
+
+%% @doc Returns `true' if `Value' is a term representing `NULL'
+-spec is_null(any(), codec()) -> boolean().
+is_null(Value, #codec{nulls = Nulls}) ->
+    lists:member(Value, Nulls).
+
 -spec type_to_oid(type(), codec()) -> epgsql_oid_db:oid().
 -spec type_to_oid(type(), codec()) -> epgsql_oid_db:oid().
 type_to_oid({array, Name}, Codec) ->
 type_to_oid({array, Name}, Codec) ->
     type_to_oid(Name, true, Codec);
     type_to_oid(Name, true, Codec);
@@ -117,28 +151,30 @@ decode(Bin, {Fun, TypeName, State}) ->
 oid_to_decoder(?RECORD_OID, binary, Codec) ->
 oid_to_decoder(?RECORD_OID, binary, Codec) ->
     {fun ?MODULE:decode_record/3, record, Codec};
     {fun ?MODULE:decode_record/3, record, Codec};
 oid_to_decoder(?RECORD_ARRAY_OID, binary, Codec) ->
 oid_to_decoder(?RECORD_ARRAY_OID, binary, Codec) ->
-    %% See `make_array_decoder/3'
-    {fun ?MODULE:decode_array/3, [], oid_to_decoder(?RECORD_OID, binary, Codec)};
-oid_to_decoder(Oid, Format, #codec{oid_db = Db}) ->
+    {fun ?MODULE:decode_array/3, array,
+     #array_decoder{
+        element_decoder = oid_to_decoder(?RECORD_OID, binary, Codec),
+        null_term = null(Codec)}};
+oid_to_decoder(Oid, Format, #codec{oid_db = Db} = Codec) ->
     case epgsql_oid_db:find_by_oid(Oid, Db) of
     case epgsql_oid_db:find_by_oid(Oid, Db) of
         undefined when Format == binary ->
         undefined when Format == binary ->
             {fun epgsql_codec_noop:decode/3, undefined, []};
             {fun epgsql_codec_noop:decode/3, undefined, []};
         undefined when Format == text ->
         undefined when Format == text ->
             {fun epgsql_codec_noop:decode_text/3, undefined, []};
             {fun epgsql_codec_noop:decode_text/3, undefined, []};
         Type ->
         Type ->
-            make_decoder(Type, Format)
+            make_decoder(Type, Format, Codec)
     end.
     end.
 
 
--spec make_decoder(epgsql_oid_db:type_info(), binary | text) -> decoder().
-make_decoder(Type, Format) ->
+-spec make_decoder(epgsql_oid_db:type_info(), binary | text, codec()) -> decoder().
+make_decoder(Type, Format, Codec) ->
     {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
     {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
     {_Oid, Name, IsArray} = epgsql_oid_db:type_to_oid_info(Type),
     {_Oid, Name, IsArray} = epgsql_oid_db:type_to_oid_info(Type),
-    make_decoder(Name, Mod, State, Format, IsArray).
+    make_decoder(Name, Mod, State, Codec, Format, IsArray).
 
 
-make_decoder(_Name, _Mod, _State, text, true) ->
+make_decoder(_Name, _Mod, _State, _Codec, text, true) ->
     %% Don't try to decode text arrays
     %% Don't try to decode text arrays
     {fun epgsql_codec_noop:decode_text/3, undefined, []};
     {fun epgsql_codec_noop:decode_text/3, undefined, []};
-make_decoder(Name, Mod, State, text, false) ->
+make_decoder(Name, Mod, State, _Codec, text, false) ->
     %% decode_text/3 is optional callback. If it's not defined, do NOOP.
     %% decode_text/3 is optional callback. If it's not defined, do NOOP.
     case erlang:function_exported(Mod, decode_text, 3) of
     case erlang:function_exported(Mod, decode_text, 3) of
         true ->
         true ->
@@ -146,18 +182,18 @@ make_decoder(Name, Mod, State, text, false) ->
         false ->
         false ->
             {fun epgsql_codec_noop:decode_text/3, undefined, []}
             {fun epgsql_codec_noop:decode_text/3, undefined, []}
     end;
     end;
-make_decoder(Name, Mod, State, binary, true) ->
-    make_array_decoder(Name, Mod, State);
-make_decoder(Name, Mod, State, binary, false) ->
+make_decoder(Name, Mod, State, Codec, binary, true) ->
+    {fun ?MODULE:decode_array/3, array,
+     #array_decoder{
+        element_decoder = {fun Mod:decode/3, Name, State},
+        null_term = null(Codec)}};
+make_decoder(Name, Mod, State, _Codec, binary, false) ->
     {fun Mod:decode/3, Name, State}.
     {fun Mod:decode/3, Name, State}.
 
 
 
 
 %% Array decoding
 %% Array decoding
 %%% $PG$/src/backend/utils/adt/arrayfuncs.c
 %%% $PG$/src/backend/utils/adt/arrayfuncs.c
-make_array_decoder(Name, Mod, State) ->
-    {fun ?MODULE:decode_array/3, [], {fun Mod:decode/3, Name, State}}.
-
-decode_array(<<NDims:?int32, _HasNull:?int32, _Oid:?int32, Rest/binary>>, _, ElemDecoder) ->
+decode_array(<<NDims:?int32, _HasNull:?int32, _Oid:?int32, Rest/binary>>, _, ArrayDecoder) ->
     %% 4b: n_dimensions;
     %% 4b: n_dimensions;
     %% 4b: flags;
     %% 4b: flags;
     %% 4b: Oid // should be the same as in column spec;
     %% 4b: Oid // should be the same as in column spec;
@@ -168,27 +204,29 @@ decode_array(<<NDims:?int32, _HasNull:?int32, _Oid:?int32, Rest/binary>>, _, Ele
     %% https://www.postgresql.org/docs/current/static/arrays.html#arrays-io
     %% https://www.postgresql.org/docs/current/static/arrays.html#arrays-io
     {Dims, Data} = erlang:split_binary(Rest, NDims * 2 * 4),
     {Dims, Data} = erlang:split_binary(Rest, NDims * 2 * 4),
     Lengths = [Len || <<Len:?int32, _LBound:?int32>> <= Dims],
     Lengths = [Len || <<Len:?int32, _LBound:?int32>> <= Dims],
-    {Array, <<>>} = decode_array1(Data, Lengths, ElemDecoder),
+    {Array, <<>>} = decode_array1(Data, Lengths, ArrayDecoder),
     Array.
     Array.
 
 
 decode_array1(Data, [], _)  ->
 decode_array1(Data, [], _)  ->
     %% zero-dimensional array
     %% zero-dimensional array
     {[], Data};
     {[], Data};
-decode_array1(Data, [Len], ElemDecoder) ->
+decode_array1(Data, [Len], ArrayDecoder) ->
     %% 1-dimensional array
     %% 1-dimensional array
-    decode_elements(Data, [], Len, ElemDecoder);
-decode_array1(Data, [Len | T], ElemDecoder) ->
+    decode_elements(Data, [], Len, ArrayDecoder);
+decode_array1(Data, [Len | T], ArrayDecoder) ->
     %% multidimensional array
     %% multidimensional array
-    F = fun(_N, Rest) -> decode_array1(Rest, T, ElemDecoder) end,
+    F = fun(_N, Rest) -> decode_array1(Rest, T, ArrayDecoder) end,
     lists:mapfoldl(F, Data, lists:seq(1, Len)).
     lists:mapfoldl(F, Data, lists:seq(1, Len)).
 
 
-decode_elements(Rest, Acc, 0, _ElDec) ->
+decode_elements(Rest, Acc, 0, _ArDec) ->
     {lists:reverse(Acc), Rest};
     {lists:reverse(Acc), Rest};
-decode_elements(<<-1:?int32, Rest/binary>>, Acc, N, ElDec) ->
-    decode_elements(Rest, [null | Acc], N - 1, ElDec);
-decode_elements(<<Len:?int32, Value:Len/binary, Rest/binary>>, Acc, N, ElemDecoder) ->
+decode_elements(<<-1:?int32, Rest/binary>>, Acc, N,
+                #array_decoder{null_term = Null} = ArDec) ->
+    decode_elements(Rest, [Null | Acc], N - 1, ArDec);
+decode_elements(<<Len:?int32, Value:Len/binary, Rest/binary>>, Acc, N,
+                #array_decoder{element_decoder = ElemDecoder} = ArDecoder) ->
     Value2 = decode(Value, ElemDecoder),
     Value2 = decode(Value, ElemDecoder),
-    decode_elements(Rest, [Value2 | Acc], N - 1, ElemDecoder).
+    decode_elements(Rest, [Value2 | Acc], N - 1, ArDecoder).
 
 
 
 
 
 
@@ -199,7 +237,7 @@ decode_record(<<Size:?int32, Bin/binary>>, record, Codec) ->
 
 
 decode_record1(<<>>, 0, _Codec) -> [];
 decode_record1(<<>>, 0, _Codec) -> [];
 decode_record1(<<_Type:?int32, -1:?int32, Rest/binary>>, Size, Codec) ->
 decode_record1(<<_Type:?int32, -1:?int32, Rest/binary>>, Size, Codec) ->
-    [null | decode_record1(Rest, Size - 1, Codec)];
+    [null(Codec) | decode_record1(Rest, Size - 1, Codec)];
 decode_record1(<<Oid:?int32, Len:?int32, ValueBin:Len/binary, Rest/binary>>, Size, Codec) ->
 decode_record1(<<Oid:?int32, Len:?int32, ValueBin:Len/binary, Rest/binary>>, Size, Codec) ->
     Value = decode(ValueBin, oid_to_decoder(Oid, binary, Codec)),
     Value = decode(ValueBin, oid_to_decoder(Oid, binary, Codec)),
     [Value | decode_record1(Rest, Size - 1, Codec)].
     [Value | decode_record1(Rest, Size - 1, Codec)].
@@ -213,44 +251,73 @@ decode_record1(<<Oid:?int32, Len:?int32, ValueBin:Len/binary, Rest/binary>>, Siz
 -spec encode(epgsql:type_name() | {array, epgsql:type_name()}, any(), codec()) -> iolist().
 -spec encode(epgsql:type_name() | {array, epgsql:type_name()}, any(), codec()) -> iolist().
 encode(TypeName, Value, Codec) ->
 encode(TypeName, Value, Codec) ->
     Type = type_to_type_info(TypeName, Codec),
     Type = type_to_type_info(TypeName, Codec),
-    encode_with_type(Type, Value).
+    encode_with_type(Type, Value, Codec).
 
 
-encode_with_type(Type, Value) ->
-    {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
+encode_with_type(Type, Value, Codec) ->
+    NameModState = epgsql_oid_db:type_to_codec_entry(Type),
     case epgsql_oid_db:type_to_oid_info(Type) of
     case epgsql_oid_db:type_to_oid_info(Type) of
         {_ArrayOid, _, true} ->
         {_ArrayOid, _, true} ->
             %FIXME: check if this OID is the same as was returned by 'Describe'
             %FIXME: check if this OID is the same as was returned by 'Describe'
             ElementOid = epgsql_oid_db:type_to_element_oid(Type),
             ElementOid = epgsql_oid_db:type_to_element_oid(Type),
-            encode_array(Value, ElementOid, {Mod, Name, State});
+            encode_array(Value, ElementOid,
+                         #array_encoder{
+                            element_encoder = NameModState,
+                            codec = Codec});
         {_Oid, _, false} ->
         {_Oid, _, false} ->
-            encode_value(Value, {Mod, Name, State})
+            encode_value(Value, NameModState)
     end.
     end.
 
 
-encode_value(Value, {Mod, Name, State}) ->
+encode_value(Value, {Name, Mod, State}) ->
     Payload = epgsql_codec:encode(Mod, Value, Name, State),
     Payload = epgsql_codec:encode(Mod, Value, Name, State),
     [<<(iolist_size(Payload)):?int32>> | Payload].
     [<<(iolist_size(Payload)):?int32>> | Payload].
 
 
 
 
 %% Number of dimensions determined at encode-time by introspection of data, so,
 %% Number of dimensions determined at encode-time by introspection of data, so,
 %% we can't encode array of lists (eg. strings).
 %% we can't encode array of lists (eg. strings).
-encode_array(Array, Oid, ValueEncoder) ->
-    {Data, {NDims, Lengths}} = encode_array(Array, 0, [], ValueEncoder),
+encode_array(Array, Oid, ArrayEncoder) ->
+    {Data, {NDims, Lengths, HasNull}} = encode_array_dims(Array, ArrayEncoder),
     Lens = [<<N:?int32, 1:?int32>> || N <- lists:reverse(Lengths)],
     Lens = [<<N:?int32, 1:?int32>> || N <- lists:reverse(Lengths)],
-    Hdr  = <<NDims:?int32, 0:?int32, Oid:?int32>>,
+    HasNullInt = case HasNull of
+                     true -> 1;
+                     false -> 0
+                 end,
+    Hdr  = <<NDims:?int32, HasNullInt:?int32, Oid:?int32>>,
     Payload  = [Hdr, Lens, Data],
     Payload  = [Hdr, Lens, Data],
     [<<(iolist_size(Payload)):?int32>> | Payload].
     [<<(iolist_size(Payload)):?int32>> | Payload].
 
 
-encode_array([], NDims, Lengths, _Codec) ->
-    {[], {NDims, Lengths}};
-encode_array([H | _] = Array, NDims, Lengths, ValueEncoder) when not is_list(H) ->
-    F = fun(E, Len) -> {encode_value(E, ValueEncoder), Len + 1} end,
-    {Data, Len} = lists:mapfoldl(F, 0, Array),
-    {Data, {NDims + 1, [Len | Lengths]}};
-encode_array(Array, NDims, Lengths, Codec) ->
-    Lengths2 = [length(Array) | Lengths],
-    F = fun(A2, {_NDims, _Lengths}) -> encode_array(A2, NDims, Lengths2, Codec) end,
-    {Data, {NDims2, Lengths3}} = lists:mapfoldl(F, {NDims, Lengths2}, Array),
-    {Data, {NDims2 + 1, Lengths3}}.
+encode_array_dims([], #array_encoder{n_dims = NDims,
+                                     lengths = Lengths,
+                                     has_null = HasNull}) ->
+    {[], {NDims, Lengths, HasNull}};
+encode_array_dims([H | _] = Array,
+                  #array_encoder{n_dims = NDims0,
+                                 lengths = Lengths0,
+                                 has_null = HasNull0,
+                                 codec = Codec,
+                                 element_encoder = ValueEncoder}) when not is_list(H) ->
+    F = fun(El, {Len, HasNull1}) ->
+                case is_null(El, Codec) of
+                    false ->
+                        {encode_value(El, ValueEncoder), {Len + 1, HasNull1}};
+                    true ->
+                        {<<-1:?int32>>, {Len + 1, true}}
+                end
+        end,
+    {Data, {Len, HasNull2}} = lists:mapfoldl(F, {0, HasNull0}, Array),
+    {Data, {NDims0 + 1, [Len | Lengths0], HasNull2}};
+encode_array_dims(Array, #array_encoder{lengths = Lengths0,
+                                        n_dims = NDims0,
+                                        has_null = HasNull0} = ArrayEncoder) ->
+    Lengths1 = [length(Array) | Lengths0],
+    F = fun(A2, {_NDims, _Lengths, HasNull1}) ->
+                encode_array_dims(A2, ArrayEncoder#array_encoder{
+                                   n_dims = NDims0,
+                                   has_null = HasNull1,
+                                   lengths = Lengths1})
+        end,
+    {Data, {NDims2, Lengths2, HasNull2}} =
+        lists:mapfoldl(F, {NDims0, Lengths1, HasNull0}, Array),
+    {Data, {NDims2 + 1, Lengths2, HasNull2}}.
 
 
 
 
 %% Supports
 %% Supports

+ 3 - 0
src/epgsql_codec.erl

@@ -1,9 +1,12 @@
 %%% @doc
 %%% @doc
 %%% Behaviour for postgresql datatype codecs.
 %%% Behaviour for postgresql datatype codecs.
+%%%
 %%% XXX: this module and callbacks "know nothing" about OIDs.
 %%% XXX: this module and callbacks "know nothing" about OIDs.
 %%% XXX: state of codec shouldn't leave epgsql_sock process. If you need to
 %%% XXX: state of codec shouldn't leave epgsql_sock process. If you need to
 %%% return "pointer" to data type/codec, it's better to return OID or type name.
 %%% return "pointer" to data type/codec, it's better to return OID or type name.
 %%% @end
 %%% @end
+%%% @see epgsql_binary
+%%% @end
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 %%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
 
 
 -module(epgsql_codec).
 -module(epgsql_codec).

+ 4 - 2
src/epgsql_command.erl

@@ -1,5 +1,5 @@
-%%% Behaviour module for epgsql_sock commands.
-%%%
+%%% @doc Behaviour module for epgsql_sock commands.
+%%% @end
 %%% Copyright (C) 2017 - Sergey Prokhorov.  All rights reserved.
 %%% Copyright (C) 2017 - Sergey Prokhorov.  All rights reserved.
 
 
 -module(epgsql_command).
 -module(epgsql_command).
@@ -16,6 +16,7 @@
 -type execute_return() ::
 -type execute_return() ::
         {ok, epgsql_sock:pg_sock(), state()}
         {ok, epgsql_sock:pg_sock(), state()}
       | {stop, Reason :: any(), Response :: any(), epgsql_sock:pg_sock()}.
       | {stop, Reason :: any(), Response :: any(), epgsql_sock:pg_sock()}.
+
 %% Execute command. It should send commands to socket.
 %% Execute command. It should send commands to socket.
 %% May be called many times if 'handle_message' will return 'requeue'.
 %% May be called many times if 'handle_message' will return 'requeue'.
 -callback execute(epgsql_sock:pg_sock(), state()) -> execute_return().
 -callback execute(epgsql_sock:pg_sock(), state()) -> execute_return().
@@ -48,6 +49,7 @@
         %% Unknown packet. Terminate `epgsql_sock' process
         %% Unknown packet. Terminate `epgsql_sock' process
       | unknown.
       | unknown.
 %% Handle incoming packet
 %% Handle incoming packet
+
 -callback handle_message(Type :: byte(), Payload :: binary() | epgsql:query_error(),
 -callback handle_message(Type :: byte(), Payload :: binary() | epgsql:query_error(),
                          epgsql_sock:pg_sock(), state()) -> handle_message_return().
                          epgsql_sock:pg_sock(), state()) -> handle_message_return().
 
 

+ 19 - 2
src/epgsql_errcodes.erl

@@ -1,4 +1,4 @@
-%% DO NOT EDIT - AUTOGENERATED BY ./generate_errcodes_src.sh ON 2018-02-23T11:18:01+0100
+%% DO NOT EDIT - AUTOGENERATED BY ./generate_errcodes_src.sh ON 2020-02-01T00:16:09+0100
 -module(epgsql_errcodes).
 -module(epgsql_errcodes).
 -export([to_name/1]).
 -export([to_name/1]).
 
 
@@ -55,7 +55,7 @@ to_name(<<"22025">>) -> invalid_escape_sequence;
 to_name(<<"22P06">>) -> nonstandard_use_of_escape_character;
 to_name(<<"22P06">>) -> nonstandard_use_of_escape_character;
 to_name(<<"22010">>) -> invalid_indicator_parameter_value;
 to_name(<<"22010">>) -> invalid_indicator_parameter_value;
 to_name(<<"22023">>) -> invalid_parameter_value;
 to_name(<<"22023">>) -> invalid_parameter_value;
-to_name(<<"22013">>) -> invalid_preceding_following_size;
+to_name(<<"22013">>) -> invalid_preceding_or_following_size;
 to_name(<<"2201B">>) -> invalid_regular_expression;
 to_name(<<"2201B">>) -> invalid_regular_expression;
 to_name(<<"2201W">>) -> invalid_row_count_in_limit_clause;
 to_name(<<"2201W">>) -> invalid_row_count_in_limit_clause;
 to_name(<<"2201X">>) -> invalid_row_count_in_result_offset_clause;
 to_name(<<"2201X">>) -> invalid_row_count_in_result_offset_clause;
@@ -84,6 +84,22 @@ to_name(<<"2200M">>) -> invalid_xml_document;
 to_name(<<"2200N">>) -> invalid_xml_content;
 to_name(<<"2200N">>) -> invalid_xml_content;
 to_name(<<"2200S">>) -> invalid_xml_comment;
 to_name(<<"2200S">>) -> invalid_xml_comment;
 to_name(<<"2200T">>) -> invalid_xml_processing_instruction;
 to_name(<<"2200T">>) -> invalid_xml_processing_instruction;
+to_name(<<"22030">>) -> duplicate_json_object_key_value;
+to_name(<<"22031">>) -> invalid_argument_for_json_datetime_function;
+to_name(<<"22032">>) -> invalid_json_text;
+to_name(<<"22033">>) -> invalid_sql_json_subscript;
+to_name(<<"22034">>) -> more_than_one_sql_json_item;
+to_name(<<"22035">>) -> no_sql_json_item;
+to_name(<<"22036">>) -> non_numeric_sql_json_item;
+to_name(<<"22037">>) -> non_unique_keys_in_a_json_object;
+to_name(<<"22038">>) -> singleton_sql_json_item_required;
+to_name(<<"22039">>) -> sql_json_array_not_found;
+to_name(<<"2203A">>) -> sql_json_member_not_found;
+to_name(<<"2203B">>) -> sql_json_number_not_found;
+to_name(<<"2203C">>) -> sql_json_object_not_found;
+to_name(<<"2203D">>) -> too_many_json_array_elements;
+to_name(<<"2203E">>) -> too_many_json_object_members;
+to_name(<<"2203F">>) -> sql_json_scalar_required;
 to_name(<<"23000">>) -> integrity_constraint_violation;
 to_name(<<"23000">>) -> integrity_constraint_violation;
 to_name(<<"23001">>) -> restrict_violation;
 to_name(<<"23001">>) -> restrict_violation;
 to_name(<<"23502">>) -> not_null_violation;
 to_name(<<"23502">>) -> not_null_violation;
@@ -195,6 +211,7 @@ to_name(<<"55000">>) -> object_not_in_prerequisite_state;
 to_name(<<"55006">>) -> object_in_use;
 to_name(<<"55006">>) -> object_in_use;
 to_name(<<"55P02">>) -> cant_change_runtime_param;
 to_name(<<"55P02">>) -> cant_change_runtime_param;
 to_name(<<"55P03">>) -> lock_not_available;
 to_name(<<"55P03">>) -> lock_not_available;
+to_name(<<"55P04">>) -> unsafe_new_enum_value_usage;
 to_name(<<"57000">>) -> operator_intervention;
 to_name(<<"57000">>) -> operator_intervention;
 to_name(<<"57014">>) -> query_canceled;
 to_name(<<"57014">>) -> query_canceled;
 to_name(<<"57P01">>) -> admin_shutdown;
 to_name(<<"57P01">>) -> admin_shutdown;

+ 3 - 3
src/epgsql_oid_db.erl

@@ -1,7 +1,7 @@
 %%% @author Sergey Prokhorov <me@seriyps.ru>
 %%% @author Sergey Prokhorov <me@seriyps.ru>
 %%% @doc
 %%% @doc
-%%% Holds Oid <-> Type mappings (forward and reverse).
-%%% See https://www.postgresql.org/docs/current/static/catalog-pg-type.html
+%%% Holds Oid to Type mappings (forward and reverse).
+%%% See [https://www.postgresql.org/docs/current/static/catalog-pg-type.html].
 %%% @end
 %%% @end
 
 
 -module(epgsql_oid_db).
 -module(epgsql_oid_db).
@@ -36,7 +36,7 @@
 %% pg_type Data preparation
 %% pg_type Data preparation
 %%
 %%
 
 
-%% @doc build query to fetch OID<->type_name information from PG server
+%% @doc build query to fetch OID to type_name information from PG server
 -spec build_query([epgsql:type_name() | binary()]) -> iolist().
 -spec build_query([epgsql:type_name() | binary()]) -> iolist().
 build_query(TypeNames) ->
 build_query(TypeNames) ->
     %% TODO: lists:join/2, ERL 19+
     %% TODO: lists:join/2, ERL 19+

+ 7 - 5
src/epgsql_scram.erl

@@ -1,11 +1,13 @@
 %%% coding: utf-8
 %%% coding: utf-8
 %%% @doc
 %%% @doc
 %%% SCRAM--SHA-256 helper functions
 %%% SCRAM--SHA-256 helper functions
-%%% See
-%%% https://www.postgresql.org/docs/current/static/sasl-authentication.html
-%%% https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism
-%%% https://tools.ietf.org/html/rfc7677
-%%% https://tools.ietf.org/html/rfc5802
+%%%
+%%% <ul>
+%%%  <li>[https://www.postgresql.org/docs/current/static/sasl-authentication.html]</li>
+%%%  <li>[https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism]</li>
+%%%  <li>[https://tools.ietf.org/html/rfc7677]</li>
+%%%  <li>[https://tools.ietf.org/html/rfc5802]</li>
+%%% </ul>
 %%% @end
 %%% @end
 
 
 -module(epgsql_scram).
 -module(epgsql_scram).

+ 30 - 13
src/epgsql_sock.erl

@@ -1,25 +1,39 @@
-%%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
-%%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
-
-%%% @doc GenServer holding all connection state (including socket).
+%%% @doc GenServer holding all the connection state (including socket).
 %%%
 %%%
-%%% See https://www.postgresql.org/docs/current/static/protocol-flow.html
-%%% Commands in PostgreSQL are pipelined: you don't need to wait for reply to
-%%% be able to send next command.
+%%% See [https://www.postgresql.org/docs/current/static/protocol-flow.html]
+%%%
+%%% Commands in PostgreSQL protocol are pipelined: you don't have to wait for
+%%% reply to be able to send next command.
 %%% Commands are processed (and responses to them are generated) in FIFO order.
 %%% Commands are processed (and responses to them are generated) in FIFO order.
 %%% eg, if you execute 2 SimpleQuery: #1 and #2, first you get all response
 %%% eg, if you execute 2 SimpleQuery: #1 and #2, first you get all response
 %%% packets for #1 and then all for #2:
 %%% packets for #1 and then all for #2:
+%%% ```
 %%% > SQuery #1
 %%% > SQuery #1
 %%% > SQuery #2
 %%% > SQuery #2
 %%% < RowDescription #1
 %%% < RowDescription #1
-%%% < DataRow #1
+%%% < DataRow #1.1
+%%% < ...
+%%% < DataRow #1.N
 %%% < CommandComplete #1
 %%% < CommandComplete #1
 %%% < RowDescription #2
 %%% < RowDescription #2
-%%% < DataRow #2
+%%% < DataRow #2.1
+%%% < ...
+%%% < DataRow #2.N
 %%% < CommandComplete #2
 %%% < CommandComplete #2
-%%%
-%%% See epgsql_cmd_connect for network connection and authentication setup
-
+%%% '''
+%%% `epgsql_sock' is capable of utilizing the pipelining feature - as soon as
+%%% it receives a new command, it sends it to the server immediately and then
+%%% it puts command's callbacks and state into internal queue of all the commands
+%%% which were sent to the server and waiting for response. So it knows in which
+%%% order it should call each pipelined command's `handle_message' callback.
+%%% But it can be easily broken if high-level command is poorly implemented or
+%%% some conflicting low-level commands (such as `parse', `bind', `execute') are
+%%% executed in a wrong order. In this case server and epgsql states become out of
+%%% sync and {@link epgsql_cmd_sync} have to be executed in order to recover.
+%%% @see epgsql_cmd_connect. epgsql_cmd_connect for network connection and authentication setup
+%%% @end
+%%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
+%%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
 
 -module(epgsql_sock).
 -module(epgsql_sock).
 
 
@@ -46,7 +60,7 @@
          get_parameter_internal/2,
          get_parameter_internal/2,
          get_replication_state/1, set_packet_handler/2]).
          get_replication_state/1, set_packet_handler/2]).
 
 
--export_type([transport/0, pg_sock/0]).
+-export_type([transport/0, pg_sock/0, error/0]).
 
 
 -include("epgsql.hrl").
 -include("epgsql.hrl").
 -include("protocol.hrl").
 -include("protocol.hrl").
@@ -59,6 +73,8 @@
 -type tcp_socket() :: port(). %gen_tcp:socket() isn't exported prior to erl 18
 -type tcp_socket() :: port(). %gen_tcp:socket() isn't exported prior to erl 18
 -type repl_state() :: #repl{}.
 -type repl_state() :: #repl{}.
 
 
+-type error() :: {error, sync_required | closed | sock_closed | sock_error}.
+
 -record(state, {mod :: gen_tcp | ssl | undefined,
 -record(state, {mod :: gen_tcp | ssl | undefined,
                 sock :: tcp_socket() | ssl:sslsocket() | undefined,
                 sock :: tcp_socket() | ssl:sslsocket() | undefined,
                 data = <<>>,
                 data = <<>>,
@@ -205,6 +221,7 @@ handle_cast({{Method, From, Ref} = Transport, Command, Args}, State)
     command_new(Transport, Command, Args, State);
     command_new(Transport, Command, Args, State);
 
 
 handle_cast(stop, State) ->
 handle_cast(stop, State) ->
+    send(State, ?TERMINATE, []),
     {stop, normal, flush_queue(State, {error, closed})};
     {stop, normal, flush_queue(State, {error, closed})};
 
 
 handle_cast(cancel, State = #state{backend = {Pid, Key},
 handle_cast(cancel, State = #state{backend = {Pid, Key},

+ 27 - 15
src/epgsql_wire.erl

@@ -1,3 +1,9 @@
+%%% @doc
+%%% Interface to encoder/decoder for postgresql
+%%% <a href="https://www.postgresql.org/docs/current/protocol-message-formats.html">wire-protocol</a>
+%%%
+%%% See also `include/protocol.hrl'.
+%%% @end
 %%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
 
@@ -25,6 +31,7 @@
 
 
 -opaque row_decoder() :: {[epgsql_binary:decoder()], [epgsql:column()], epgsql_binary:codec()}.
 -opaque row_decoder() :: {[epgsql_binary:decoder()], [epgsql:column()], epgsql_binary:codec()}.
 
 
+%% @doc tries to extract single postgresql packet from TCP stream
 -spec decode_message(binary()) -> {byte(), binary(), binary()} | binary().
 -spec decode_message(binary()) -> {byte(), binary(), binary()} | binary().
 decode_message(<<Type:8, Len:?int32, Rest/binary>> = Bin) ->
 decode_message(<<Type:8, Len:?int32, Rest/binary>> = Bin) ->
     Len2 = Len - 4,
     Len2 = Len - 4,
@@ -65,7 +72,7 @@ decode_fields(<<Type:8, Rest/binary>>, Acc) ->
     decode_fields(Rest2, [{Type, Str} | Acc]).
     decode_fields(Rest2, [{Type, Str} | Acc]).
 
 
 %% @doc decode ErrorResponse
 %% @doc decode ErrorResponse
-%% See http://www.postgresql.org/docs/current/interactive/protocol-error-fields.html
+%% See [http://www.postgresql.org/docs/current/interactive/protocol-error-fields.html]
 -spec decode_error(binary()) -> epgsql:query_error().
 -spec decode_error(binary()) -> epgsql:query_error().
 decode_error(Bin) ->
 decode_error(Bin) ->
     Fields = decode_fields(Bin),
     Fields = decode_fields(Bin),
@@ -143,17 +150,17 @@ decode_data(Bin, {Decoders, _Columns, Codec}) ->
 
 
 decode_data(_, [], _) -> [];
 decode_data(_, [], _) -> [];
 decode_data(<<-1:?int32, Rest/binary>>, [_Dec | Decs], Codec) ->
 decode_data(<<-1:?int32, Rest/binary>>, [_Dec | Decs], Codec) ->
-    [null | decode_data(Rest, Decs, Codec)];
+    [epgsql_binary:null(Codec) | decode_data(Rest, Decs, Codec)];
 decode_data(<<Len:?int32, Value:Len/binary, Rest/binary>>, [Decoder | Decs], Codec) ->
 decode_data(<<Len:?int32, Value:Len/binary, Rest/binary>>, [Decoder | Decs], Codec) ->
     [epgsql_binary:decode(Value, Decoder)
     [epgsql_binary:decode(Value, Decoder)
      | decode_data(Rest, Decs, Codec)].
      | decode_data(Rest, Decs, Codec)].
 
 
-%% @doc decode column information
+%% @doc decode RowDescription column information
 -spec decode_columns(non_neg_integer(), binary(), epgsql_binary:codec()) -> [epgsql:column()].
 -spec decode_columns(non_neg_integer(), binary(), epgsql_binary:codec()) -> [epgsql:column()].
 decode_columns(0, _Bin, _Codec) -> [];
 decode_columns(0, _Bin, _Codec) -> [];
 decode_columns(Count, Bin, Codec) ->
 decode_columns(Count, Bin, Codec) ->
     [Name, Rest] = decode_string(Bin),
     [Name, Rest] = decode_string(Bin),
-    <<_TableOid:?int32, _AttribNum:?int16, TypeOid:?int32,
+    <<TableOid:?int32, AttribNum:?int16, TypeOid:?int32,
       Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest,
       Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest,
     %% TODO: get rid of this 'type' (extra oid_db lookup)
     %% TODO: get rid of this 'type' (extra oid_db lookup)
     Type = epgsql_binary:oid_to_name(TypeOid, Codec),
     Type = epgsql_binary:oid_to_name(TypeOid, Codec),
@@ -163,7 +170,9 @@ decode_columns(Count, Bin, Codec) ->
       oid      = TypeOid,
       oid      = TypeOid,
       size     = Size,
       size     = Size,
       modifier = Modifier,
       modifier = Modifier,
-      format   = Format},
+      format   = Format,
+      table_oid = TableOid,
+      table_attr_number = AttribNum},
     [Desc | decode_columns(Count - 1, Rest2, Codec)].
     [Desc | decode_columns(Count - 1, Rest2, Codec)].
 
 
 %% @doc decode ParameterDescription
 %% @doc decode ParameterDescription
@@ -175,7 +184,7 @@ decode_parameters(<<_Count:?int16, Bin/binary>>, Codec) ->
          TypeInfo -> TypeInfo
          TypeInfo -> TypeInfo
      end || <<Oid:?int32>> <= Bin].
      end || <<Oid:?int32>> <= Bin].
 
 
-%% @doc decode command complete msg
+%% @doc decode CcommandComplete msg
 decode_complete(<<"SELECT", 0>>)        -> select;
 decode_complete(<<"SELECT", 0>>)        -> select;
 decode_complete(<<"SELECT", _/binary>>) -> select;
 decode_complete(<<"SELECT", _/binary>>) -> select;
 decode_complete(<<"BEGIN", 0>>)         -> 'begin';
 decode_complete(<<"BEGIN", 0>>)         -> 'begin';
@@ -217,6 +226,7 @@ encode_formats([], Count, Acc) ->
 encode_formats([#column{format = Format} | T], Count, Acc) ->
 encode_formats([#column{format = Format} | T], Count, Acc) ->
     encode_formats(T, Count + 1, <<Acc/binary, Format:?int16>>).
     encode_formats(T, Count + 1, <<Acc/binary, Format:?int16>>).
 
 
+%% @doc Returns 1 if Codec knows how to decode binary format of the type provided and 0 otherwise
 format({unknown_oid, _}, _) -> 0;
 format({unknown_oid, _}, _) -> 0;
 format(#column{oid = Oid}, Codec) ->
 format(#column{oid = Oid}, Codec) ->
     case epgsql_binary:supports(Oid, Codec) of
     case epgsql_binary:supports(Oid, Codec) of
@@ -244,16 +254,18 @@ encode_parameters([P | T], Count, Formats, Values, Codec) ->
       Type :: epgsql:type_name()
       Type :: epgsql:type_name()
             | {array, epgsql:type_name()}
             | {array, epgsql:type_name()}
             | {unknown_oid, epgsql_oid_db:oid()}.
             | {unknown_oid, epgsql_oid_db:oid()}.
-encode_parameter({T, undefined}, Codec) ->
-    encode_parameter({T, null}, Codec);
-encode_parameter({_, null}, _Codec) ->
-    {1, <<-1:?int32>>};
-encode_parameter({{unknown_oid, _Oid}, Value}, _Codec) ->
-    {0, encode_text(Value)};
 encode_parameter({Type, Value}, Codec) ->
 encode_parameter({Type, Value}, Codec) ->
-    {1, epgsql_binary:encode(Type, Value, Codec)};
-encode_parameter(Value, _Codec) ->
-    {0, encode_text(Value)}.
+    case epgsql_binary:is_null(Value, Codec) of
+        false ->
+            encode_parameter(Type, Value, Codec);
+        true ->
+            {1, <<-1:?int32>>}
+    end.
+
+encode_parameter({unknown_oid, _Oid}, Value, _Codec) ->
+    {0, encode_text(Value)};
+encode_parameter(Type, Value, Codec) ->
+    {1, epgsql_binary:encode(Type, Value, Codec)}.
 
 
 encode_text(B) when is_binary(B)  -> encode_bin(B);
 encode_text(B) when is_binary(B)  -> encode_bin(B);
 encode_text(A) when is_atom(A)    -> encode_bin(atom_to_binary(A, utf8));
 encode_text(A) when is_atom(A)    -> encode_bin(atom_to_binary(A, utf8));

+ 12 - 1
src/epgsqla.erl

@@ -1,3 +1,10 @@
+%%% @doc
+%%% Asynchronous interface.
+%%%
+%%% All the functions return `reference()' immediately. Results are delivered
+%%% asynchronously in a form of `{connection(), reference(), Result}', where
+%%% `Result' is what synchronous version of this function normally returns.
+%%% @end
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
 
 -module(epgsqla).
 -module(epgsqla).
@@ -15,7 +22,7 @@
          describe/2, describe/3,
          describe/2, describe/3,
          bind/3, bind/4,
          bind/3, bind/4,
          execute/2, execute/3, execute/4,
          execute/2, execute/3, execute/4,
-         execute_batch/2,
+         execute_batch/2, execute_batch/3,
          close/2, close/3,
          close/2, close/3,
          sync/1,
          sync/1,
          cancel/1,
          cancel/1,
@@ -123,6 +130,10 @@ execute(C, Statement, PortalName, MaxRows) ->
 execute_batch(C, Batch) ->
 execute_batch(C, Batch) ->
     cast(C, epgsql_cmd_batch, Batch).
     cast(C, epgsql_cmd_batch, Batch).
 
 
+-spec execute_batch(epgsql:connection(), epgsql:statement(), [ [epgsql:bind_param()] ]) -> reference().
+execute_batch(C, #statement{} = Statement, Batch) ->
+    cast(C, epgsql_cmd_batch, {Statement, Batch}).
+
 describe(C, #statement{name = Name}) ->
 describe(C, #statement{name = Name}) ->
     describe(C, statement, Name).
     describe(C, statement, Name).
 
 

+ 12 - 1
src/epgsqli.erl

@@ -1,3 +1,10 @@
+%%% @doc Incremental interface
+%%%
+%%% All the functions return `reference()' immediately. Each data row as well
+%%% as metadata are delivered as separate messages in a form of
+%%% `{connection(), reference(), Payload}' where `Payload' depends on command
+%%% being executed.
+%%% @end
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
 
 -module(epgsqli).
 -module(epgsqli).
@@ -15,7 +22,7 @@
          describe/2, describe/3,
          describe/2, describe/3,
          bind/3, bind/4,
          bind/3, bind/4,
          execute/2, execute/3, execute/4,
          execute/2, execute/3, execute/4,
-         execute_batch/2,
+         execute_batch/2, execute_batch/3,
          close/2, close/3,
          close/2, close/3,
          sync/1,
          sync/1,
          cancel/1]).
          cancel/1]).
@@ -123,6 +130,10 @@ execute(C, Statement, PortalName, MaxRows) ->
 execute_batch(C, Batch) ->
 execute_batch(C, Batch) ->
     incremental(C, epgsql_cmd_batch, Batch).
     incremental(C, epgsql_cmd_batch, Batch).
 
 
+-spec execute_batch(epgsql:connection(), epgsql:statement(), [ [epgsql:bind_param()] ]) -> reference().
+execute_batch(C, #statement{} = Statement, Batch) ->
+    incremental(C, epgsql_cmd_batch, {Statement, Batch}).
+
 describe(C, #statement{name = Name}) ->
 describe(C, #statement{name = Name}) ->
     describe(C, statement, Name).
     describe(C, statement, Name).
 
 

+ 7 - 2
src/ewkb.erl

@@ -1,5 +1,10 @@
-%% https://en.wikipedia.org/wiki/Well-known_text
-%% http://postgis.net/docs/manual-2.4/using_postgis_dbmanagement.html#EWKB_EWKT
+%% @doc
+%% Encoder/decoder for PostGIS binary data representation.
+%%
+%% <ul>
+%%  <li>[https://en.wikipedia.org/wiki/Well-known_text]</li>
+%%  <li>[http://postgis.net/docs/manual-2.4/using_postgis_dbmanagement.html#EWKB_EWKT]</li>
+%% </ul>
 -module(ewkb).
 -module(ewkb).
 -export([decode_geometry/1, encode_geometry/1]).
 -export([decode_geometry/1, encode_geometry/1]).
 -export_type([point_type/0, point/1, multi_point/1, line_string/1,
 -export_type([point_type/0, point/1, multi_point/1, line_string/1,

+ 168 - 20
test/epgsql_SUITE.erl

@@ -1,6 +1,6 @@
 -module(epgsql_SUITE).
 -module(epgsql_SUITE).
 
 
--include_lib("eunit/include/eunit.hrl").
+-include_lib("stdlib/include/assert.hrl").
 -include_lib("common_test/include/ct.hrl").
 -include_lib("common_test/include/ct.hrl").
 -include_lib("public_key/include/public_key.hrl").
 -include_lib("public_key/include/public_key.hrl").
 -include("epgsql_tests.hrl").
 -include("epgsql_tests.hrl").
@@ -67,7 +67,8 @@ groups() ->
             range_type,
             range_type,
             range8_type,
             range8_type,
             date_time_range_type,
             date_time_range_type,
-            custom_types
+            custom_types,
+            custom_null
         ]},
         ]},
         {generic, [parallel], [
         {generic, [parallel], [
             with_transaction
             with_transaction
@@ -87,6 +88,9 @@ groups() ->
         cursor,
         cursor,
         multiple_result,
         multiple_result,
         execute_batch,
         execute_batch,
+        execute_batch_3_named_stmt,
+        execute_batch_3_unnamed_stmt,
+        execute_batch_3_sql,
         batch_error,
         batch_error,
         single_batch,
         single_batch,
         extended_select,
         extended_select,
@@ -108,6 +112,7 @@ groups() ->
         describe_with_param,
         describe_with_param,
         describe_named,
         describe_named,
         describe_error,
         describe_error,
+        describe_portal,
         portal,
         portal,
         returning,
         returning,
         multiple_statement,
         multiple_statement,
@@ -431,6 +436,32 @@ execute_batch(Config) ->
             Module:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}])
             Module:execute_batch(C, [{S1, [1]}, {S2, [1, 2]}])
     end).
     end).
 
 
+execute_batch_3_named_stmt(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        {ok, Stmt} = Module:parse(C, "my_stmt", "select $1 + $2", [int4, int4]),
+        ?assertMatch(
+           {[#column{type = int4, _ = _}], [{ok, [{3}]}, {ok, [{7}]}]},
+           Module:execute_batch(C, Stmt, [[1, 2], [3, 4]]))
+    end).
+
+execute_batch_3_unnamed_stmt(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        {ok, Stmt} = Module:parse(C, "select $1::integer + $2::integer"),
+        ?assertMatch(
+           {[#column{type = int4, _ = _}], [{ok, [{3}]}, {ok, [{7}]}]},
+           Module:execute_batch(C, Stmt, [[2, 1], [4, 3]]))
+    end).
+
+execute_batch_3_sql(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        ?assertMatch(
+           {[#column{type = int4, _ = _}], [{ok, [{3}]}, {ok, [{7}]}]},
+           Module:execute_batch(C, "select $1::integer + $2::integer", [[1, 2], [3, 4]]))
+    end).
+
 batch_error(Config) ->
 batch_error(Config) ->
     Module = ?config(module, Config),
     Module = ?config(module, Config),
     epgsql_ct:with_rollback(Config, fun(C) ->
     epgsql_ct:with_rollback(Config, fun(C) ->
@@ -514,9 +545,9 @@ parse_column_format(Config) ->
     Module = ?config(module, Config),
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
     epgsql_ct:with_connection(Config, fun(C) ->
         {ok, S} = Module:parse(C, "select 1::int4, false::bool, 2.0::float4"),
         {ok, S} = Module:parse(C, "select 1::int4, false::bool, 2.0::float4"),
-        [#column{type = int4},
-         #column{type = bool},
-         #column{type = float4}] = S#statement.columns,
+        [#column{type = int4, table_oid = 0, table_attr_number = 0},
+         #column{type = bool, table_oid = 0, table_attr_number = 0},
+         #column{type = float4, table_oid = 0, table_attr_number = 0}] = S#statement.columns,
         ok = Module:bind(C, S, []),
         ok = Module:bind(C, S, []),
         {ok, [{1, false, 2.0}]} = Module:execute(C, S, 0),
         {ok, [{1, false, 2.0}]} = Module:execute(C, S, 0),
         ok = Module:close(C, S),
         ok = Module:close(C, S),
@@ -652,6 +683,23 @@ describe_error(Config) ->
 
 
     end).
     end).
 
 
+describe_portal(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        {ok, Stmt} = Module:parse(C, "my_stmt", "select * from test_table1 WHERE id = $1", []),
+        ok = Module:bind(C, Stmt, "my_portal", [1]),
+        {ok, Columns} = Module:describe(C, portal, "my_portal"),
+        ?assertMatch(
+           [#column{name = <<"id">>,
+                    type = int4},
+            #column{name = <<"value">>,
+                    type = text}],
+           Columns
+          ),
+        ok = Module:close(C, Stmt),
+        ok = Module:sync(C)
+    end).
+
 portal(Config) ->
 portal(Config) ->
     Module = ?config(module, Config),
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
     epgsql_ct:with_connection(Config, fun(C) ->
@@ -732,7 +780,25 @@ numeric_type(Config) ->
     check_type(Config, float4, "1.0", 1.0, [0.0, 1.23456, -1.23456]),
     check_type(Config, float4, "1.0", 1.0, [0.0, 1.23456, -1.23456]),
     check_type(Config, float4, "'-Infinity'", minus_infinity, [minus_infinity, plus_infinity, nan]),
     check_type(Config, float4, "'-Infinity'", minus_infinity, [minus_infinity, plus_infinity, nan]),
     check_type(Config, float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]),
     check_type(Config, float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]),
-    check_type(Config, float8, "'nan'", nan, [minus_infinity, plus_infinity, nan]).
+    check_type(Config, float8, "'nan'", nan, [minus_infinity, plus_infinity, nan]),
+    %% Check overflow protection. Connection just crashes for now instead of silently
+    %% truncating the data. Some cleaner behaviour can be introduced later.
+    epgsql_ct:with_connection(Config, fun(C) ->
+        Module = ?config(module, Config),
+        Trap = process_flag(trap_exit, true),
+        try Module:equery(C, "SELECT $1::int2", [32768]) of
+          {error, closed} ->
+                %% epgsqla/epgsqli
+                ok
+        catch exit:Reason ->
+                %% epgsql
+                ?assertMatch({{{integer_overflow, int2, _}, _}, _}, Reason),
+                receive {'EXIT', C, _} -> ok
+                after 1000 -> error(timeout)
+                end
+        end,
+        process_flag(trap_exit, Trap)
+    end).
 
 
 character_type(Config) ->
 character_type(Config) ->
     Alpha = unicode:characters_to_binary([16#03B1]),
     Alpha = unicode:characters_to_binary([16#03B1]),
@@ -852,6 +918,7 @@ misc_type(Config) ->
     check_type(Config, bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]).
     check_type(Config, bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]).
 
 
 hstore_type(Config) ->
 hstore_type(Config) ->
+    Module = ?config(module, Config),
     Values = [
     Values = [
         {[]},
         {[]},
         {[{null, null}]},
         {[{null, null}]},
@@ -867,7 +934,42 @@ hstore_type(Config) ->
     check_type(Config, hstore, "''", {[]}, []),
     check_type(Config, hstore, "''", {[]}, []),
     check_type(Config, hstore,
     check_type(Config, hstore,
                "'a => 1, b => 2.0, c => null'",
                "'a => 1, b => 2.0, c => null'",
-               {[{<<"a">>, <<"1">>}, {<<"b">>, <<"2.0">>}, {<<"c">>, null}]}, Values).
+               {[{<<"a">>, <<"1">>}, {<<"b">>, <<"2.0">>}, {<<"c">>, null}]}, Values),
+    epgsql_ct:with_connection(
+      Config,
+      fun(C) ->
+              %% Maps as input
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [maps:from_list(KV)]),
+                   ?assert(compare(hstore, Res, Jiffy))
+               end || {KV} = Jiffy <- Values],
+              %% Maps as output
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => map}}]),
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [maps:from_list(KV)]),
+                   HstoreMap = maps:from_list([{format_hstore_key(K), format_hstore_value(V)} || {K, V} <- KV]),
+                   ?assertEqual(HstoreMap, Res)
+               end || {KV} <- Values],
+              %% Proplist as output
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => proplist}}]),
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [Jiffy]),
+                   HstoreProplist = [{format_hstore_key(K), format_hstore_value(V)} || {K, V} <- KV],
+                   ?assertEqual(lists:sort(HstoreProplist), lists:sort(Res))
+               end || {KV} = Jiffy <- Values],
+              %% Custom nulls
+              Nulls = [nil, 'NULL', aaaa],
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => map,
+                                                             nulls => Nulls}}]),
+              K = <<"k">>,
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [#{K => V}]),
+                   ?assertEqual(#{K => nil}, Res)
+               end || V <- Nulls]
+      end).
 
 
 net_type(Config) ->
 net_type(Config) ->
     check_type(Config, cidr, "'127.0.0.1/32'", {{127,0,0,1}, 32}, [{{127,0,0,1}, 32}, {{0,0,0,0,0,0,0,1}, 128}]),
     check_type(Config, cidr, "'127.0.0.1/32'", {{127,0,0,1}, 32}, [{{127,0,0,1}, 32}, {{0,0,0,0,0,0,0,1}, 128}]),
@@ -884,18 +986,24 @@ array_type(Config) ->
         {ok, _, [{[1, 2]}]} = Module:equery(C, "select ($1::int[])[1:2]", [[1, 2, 3]]),
         {ok, _, [{[1, 2]}]} = Module:equery(C, "select ($1::int[])[1:2]", [[1, 2, 3]]),
         {ok, _, [{[{1, <<"one">>}, {2, <<"two">>}]}]} =
         {ok, _, [{[{1, <<"one">>}, {2, <<"two">>}]}]} =
             Module:equery(C, "select Array(select (id, value) from test_table1)", []),
             Module:equery(C, "select Array(select (id, value) from test_table1)", []),
-        Select = fun(Type, A) ->
+        {ok, _, [{ [[1], [null], [3], [null]] }]} =
+            Module:equery(C, "select $1::int2[]", [ [[1], [null], [3], [undefined]] ]),
+        Select = fun(Type, AIn) ->
             Query = "select $1::" ++ atom_to_list(Type) ++ "[]",
             Query = "select $1::" ++ atom_to_list(Type) ++ "[]",
-            {ok, _Cols, [{A2}]} = Module:equery(C, Query, [A]),
-            case lists:all(fun({V, V2}) -> compare(Type, V, V2) end, lists:zip(A, A2)) of
+            {ok, _Cols, [{AOut}]} = Module:equery(C, Query, [AIn]),
+            case lists:all(fun({VIn, VOut}) ->
+                                   compare(Type, VIn, VOut)
+                           end, lists:zip(AIn, AOut)) of
                 true  -> ok;
                 true  -> ok;
-                false -> ?assertMatch(A, A2)
+                false -> ?assertEqual(AIn, AOut)
             end
             end
         end,
         end,
         Select(int2,   []),
         Select(int2,   []),
         Select(int2,   [1, 2, 3, 4]),
         Select(int2,   [1, 2, 3, 4]),
         Select(int2,   [[1], [2], [3], [4]]),
         Select(int2,   [[1], [2], [3], [4]]),
         Select(int2,   [[[[[[1, 2]]]]]]),
         Select(int2,   [[[[[[1, 2]]]]]]),
+        Select(int2,   [1, null, 3, undefined]),
+        Select(int2,   [[1], [null], [3], [null]]),
         Select(bool,   [true]),
         Select(bool,   [true]),
         Select(char,   [$a, $b, $c]),
         Select(char,   [$a, $b, $c]),
         Select(int4,   [[1, 2]]),
         Select(int4,   [[1, 2]]),
@@ -936,7 +1044,10 @@ record_type(Config) ->
         Select("select (1, '{2,3}'::int[])", {{1, [2, 3]}}),
         Select("select (1, '{2,3}'::int[])", {{1, [2, 3]}}),
 
 
         %% Array of records inside record
         %% Array of records inside record
-        Select("select (0, ARRAY(select (id, value) from test_table1))", {{0,[{1,<<"one">>},{2,<<"two">>}]}})
+        Select("select (0, ARRAY(select (id, value) from test_table1))", {{0,[{1,<<"one">>},{2,<<"two">>}]}}),
+
+        %% Record with NULLs
+        Select("select (1, NULL::integer, 2)", {{1, null, 2}})
     end).
     end).
 
 
 custom_types(Config) ->
 custom_types(Config) ->
@@ -953,6 +1064,33 @@ custom_types(Config) ->
         ?assertMatch({ok, _, [{bar}]}, Module:equery(C, "SELECT col FROM t_foo"))
         ?assertMatch({ok, _, [{bar}]}, Module:equery(C, "SELECT col FROM t_foo"))
     end).
     end).
 
 
+custom_null(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        Test3 = fun(Type, In, Out) ->
+                        Q = ["SELECT $1::", Type],
+                        {ok, _, [{Res}]} = Module:equery(C, Q, [In]),
+                        ?assertEqual(Out, Res)
+                end,
+        Test = fun(Type, In) ->
+                       Test3(Type, In, In)
+               end,
+        Test("int2", nil),
+        Test3("int2", 'NULL', nil),
+        Test("text", nil),
+        Test3("text", 'NULL', nil),
+        Test3("text", null, <<"null">>),
+        Test("int2[]", [nil, 1, nil, 2]),
+        Test3("text[]", [null, <<"ok">>], [<<"null">>, <<"ok">>]),
+        Test3("int2[]", ['NULL', 1, nil, 2], [nil, 1, nil, 2]),
+        Test("int2[]", [[nil], [1], [nil], [2]]),
+        Test3("int2[]", [['NULL'], [1], [nil], [2]], [[nil], [1], [nil], [2]]),
+        ?assertMatch(
+           {ok, _, [{ {1, nil, {2, nil, 3}} }]},
+           Module:equery(C, "SELECT (1, NULL, (2, NULL, 3))", []))
+    end,
+    [{nulls, [nil, 'NULL']}]).
+
 text_format(Config) ->
 text_format(Config) ->
     Module = ?config(module, Config),
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
     epgsql_ct:with_connection(Config, fun(C) ->
@@ -1026,7 +1164,7 @@ connection_closed_by_server(Config) ->
                     {'EXIT', C2, {shutdown, #error{code = <<"57P01">>}}} ->
                     {'EXIT', C2, {shutdown, #error{code = <<"57P01">>}}} ->
                         P ! ok;
                         P ! ok;
                     Other ->
                     Other ->
-                        ?debugFmt("Unexpected msg: ~p~n", [Other]),
+                        ct:pal("Unexpected msg: ~p~n", [Other]),
                         P ! error
                         P ! error
                 end
                 end
             end)
             end)
@@ -1186,7 +1324,8 @@ range_type(Config) ->
         check_type(Config, int4range, "int4range(10, 20)", {10, 20}, [
         check_type(Config, int4range, "int4range(10, 20)", {10, 20}, [
             {1, 58}, {-1, 12}, {-985521, 5412687}, {minus_infinity, 0},
             {1, 58}, {-1, 12}, {-985521, 5412687}, {minus_infinity, 0},
             {984655, plus_infinity}, {minus_infinity, plus_infinity}
             {984655, plus_infinity}, {minus_infinity, plus_infinity}
-        ])
+        ]),
+        check_type(Config, int4range, "int4range(10, 10)", empty, [])
    end, []).
    end, []).
 
 
 range8_type(Config) ->
 range8_type(Config) ->
@@ -1195,17 +1334,26 @@ range8_type(Config) ->
             {1, 58}, {-1, 12}, {-9223372036854775808, 5412687},
             {1, 58}, {-1, 12}, {-9223372036854775808, 5412687},
             {minus_infinity, 9223372036854775807},
             {minus_infinity, 9223372036854775807},
             {984655, plus_infinity}, {minus_infinity, plus_infinity}
             {984655, plus_infinity}, {minus_infinity, plus_infinity}
-        ])
+        ]),
+        check_type(Config, int8range, "int8range(10, 10)", empty, [])
     end, []).
     end, []).
 
 
 date_time_range_type(Config) ->
 date_time_range_type(Config) ->
     epgsql_ct:with_min_version(Config, [9, 2], fun(_C) ->
     epgsql_ct:with_min_version(Config, [9, 2], fun(_C) ->
         check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
         check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
-       check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-01-02 03:04:05')", empty, []),
-
-       check_type(Config, daterange, "daterange('2008-01-02', '2008-02-02')", {{2008,1,2}, {2008, 2, 2}}, [{{-4712,1,1}, {5874897,1,1}}
-]),
-      check_type(Config, tstzrange, "tstzrange('2011-01-02 03:04:05+3', '2011-01-02 04:04:05+3')", {{{2011, 1, 2}, {0, 4, 5.0}}, {{2011, 1, 2}, {1, 4, 5.0}}}, [{{{2011, 1, 2}, {0, 4, 5.0}}, {{2011, 1, 2}, {1, 4, 5.0}}}])
+        check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05', '[]')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
+        check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05', '()')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
+        check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05', '[)')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
+        check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-02-02 03:04:05', '(]')", {{{2008,1,2},{3,4,5.0}}, {{2008,2,2},{3,4,5.0}}}, []),
+        check_type(Config, tsrange, "tsrange('2008-01-02 03:04:05', '2008-01-02 03:04:05')", empty, []),
+        check_type(Config, daterange, "daterange('2008-01-02', '2008-02-02')", {{2008,1,2}, {2008, 2, 2}}, [{{-4712,1,1}, {5874897,1,1}}]),
+        check_type(Config, tstzrange, "tstzrange('2011-01-02 03:04:05+3', '2011-01-02 04:04:05+3')", {{{2011, 1, 2}, {0, 4, 5.0}}, {{2011, 1, 2}, {1, 4, 5.0}}}, [{{{2011, 1, 2}, {0, 4, 5.0}}, {{2011, 1, 2}, {1, 4, 5.0}}}]),
+        check_type(Config, tstzrange, "tstzrange('2008-01-02 03:04:05', null)", {{{2008,1,2},{3,4,5.0}}, plus_infinity}, []),
+        check_type(Config, tstzrange, "tstzrange('2008-01-02 03:04:05', null, '[]')", {{{2008,1,2},{3,4,5.0}}, plus_infinity}, []),
+        check_type(Config, tstzrange, "tstzrange('2008-01-02 03:04:05', null, '()')", {{{2008,1,2},{3,4,5.0}}, plus_infinity}, []),
+        check_type(Config, tstzrange, "tstzrange(null, '2008-01-02 03:04:05')", {minus_infinity, {{2008,1,2},{3,4,5.0}}}, []),
+        check_type(Config, tstzrange, "tstzrange(null, '2008-01-02 03:04:05', '[]')", {minus_infinity, {{2008,1,2},{3,4,5.0}}}, []),
+        check_type(Config, tstzrange, "tstzrange(null, '2008-01-02 03:04:05', '()')", {minus_infinity, {{2008,1,2},{3,4,5.0}}}, [])
 
 
    end, []).
    end, []).
 
 

+ 12 - 1
test/epgsql_cast.erl

@@ -9,7 +9,7 @@
 -export([get_parameter/2, set_notice_receiver/2, get_cmd_status/1, squery/2, equery/2, equery/3]).
 -export([get_parameter/2, set_notice_receiver/2, get_cmd_status/1, squery/2, equery/2, equery/3]).
 -export([prepared_query/3]).
 -export([prepared_query/3]).
 -export([parse/2, parse/3, parse/4, describe/2, describe/3]).
 -export([parse/2, parse/3, parse/4, describe/2, describe/3]).
--export([bind/3, bind/4, execute/2, execute/3, execute/4, execute_batch/2]).
+-export([bind/3, bind/4, execute/2, execute/3, execute/4, execute_batch/2, execute_batch/3]).
 -export([close/2, close/3, sync/1]).
 -export([close/2, close/3, sync/1]).
 -export([receive_result/2, sync_on_error/2]).
 -export([receive_result/2, sync_on_error/2]).
 
 
@@ -123,6 +123,17 @@ execute_batch(C, Batch) ->
     Ref = epgsqla:execute_batch(C, Batch),
     Ref = epgsqla:execute_batch(C, Batch),
     receive_result(C, Ref).
     receive_result(C, Ref).
 
 
+execute_batch(C, #statement{columns = Cols} = Stmt, Batch) ->
+    Ref = epgsqla:execute_batch(C, Stmt, Batch),
+    {Cols, receive_result(C, Ref)};
+execute_batch(C, Sql, Batch) ->
+    case parse(C, Sql) of
+        {ok, #statement{} = S} ->
+            execute_batch(C, S, Batch);
+        Error ->
+            Error
+    end.
+
 %% statement/portal functions
 %% statement/portal functions
 
 
 describe(C, #statement{name = Name}) ->
 describe(C, #statement{name = Name}) ->

+ 27 - 4
test/epgsql_incremental.erl

@@ -9,7 +9,7 @@
 -export([get_parameter/2, set_notice_receiver/2, get_cmd_status/1, squery/2, equery/2, equery/3]).
 -export([get_parameter/2, set_notice_receiver/2, get_cmd_status/1, squery/2, equery/2, equery/3]).
 -export([prepared_query/3]).
 -export([prepared_query/3]).
 -export([parse/2, parse/3, parse/4, describe/2, describe/3]).
 -export([parse/2, parse/3, parse/4, describe/2, describe/3]).
--export([bind/3, bind/4, execute/2, execute/3, execute/4, execute_batch/2]).
+-export([bind/3, bind/4, execute/2, execute/3, execute/4, execute_batch/2, execute_batch/3]).
 -export([close/2, close/3, sync/1]).
 -export([close/2, close/3, sync/1]).
 
 
 -include("epgsql.hrl").
 -include("epgsql.hrl").
@@ -124,6 +124,17 @@ execute_batch(C, Batch) ->
     Ref = epgsqli:execute_batch(C, Batch),
     Ref = epgsqli:execute_batch(C, Batch),
     receive_extended_results(C, Ref, []).
     receive_extended_results(C, Ref, []).
 
 
+execute_batch(C, #statement{columns = Cols} = Stmt, Batch) ->
+    Ref = epgsqli:execute_batch(C, Stmt, Batch),
+    {Cols, receive_extended_results(C, Ref, [])};
+execute_batch(C, Sql, Batch) ->
+    case parse(C, Sql) of
+        {ok, #statement{} = S} ->
+            execute_batch(C, S, Batch);
+        Error ->
+            Error
+    end.
+
 %% statement/portal functions
 %% statement/portal functions
 
 
 describe(C, #statement{name = Name}) ->
 describe(C, #statement{name = Name}) ->
@@ -133,9 +144,9 @@ describe(C, statement, Name) ->
     Ref = epgsqli:describe(C, statement, Name),
     Ref = epgsqli:describe(C, statement, Name),
     sync_on_error(C, receive_describe(C, Ref, #statement{name = Name}));
     sync_on_error(C, receive_describe(C, Ref, #statement{name = Name}));
 
 
-describe(C, Type, Name) ->
-    %% TODO unknown result format of Describe portal
-    epgsqli:describe(C, Type, Name).
+describe(C, portal, Name) ->
+    Ref = epgsqli:describe(C, portal, Name),
+    sync_on_error(C, receive_describe_portal(C, Ref)).
 
 
 close(C, #statement{name = Name}) ->
 close(C, #statement{name = Name}) ->
     close(C, statement, Name).
     close(C, statement, Name).
@@ -230,6 +241,18 @@ receive_describe(C, Ref, Statement = #statement{}) ->
             {error, closed}
             {error, closed}
     end.
     end.
 
 
+receive_describe_portal(C, Ref) ->
+    receive
+        {C, Ref, {columns, Columns}} ->
+            {ok, Columns};
+        {C, Ref, no_data} ->
+            {ok, []};
+        {C, Ref, Error = {error, _}} ->
+            Error;
+        {'EXIT', C, _Reason} ->
+            {error, closed}
+    end.
+
 receive_atom(C, Ref, Receive, Return) ->
 receive_atom(C, Ref, Receive, Return) ->
     receive
     receive
         {C, Ref, Receive} ->
         {C, Ref, Receive} ->