Browse Source

Merge remote-tracking branch 'upstream/devel' into repl_fix

Mikhail Kalashnikov 6 years ago
parent
commit
471e4cfebb
66 changed files with 4375 additions and 1873 deletions
  1. 5 7
      .travis.yml
  2. 73 0
      CHANGES
  3. 11 4
      Makefile
  4. 116 71
      README.md
  5. 1 1
      generate_errcodes_src.sh
  6. 4 4
      include/epgsql.hrl
  7. 0 3
      include/epgsql_binary.hrl
  8. 13 61
      include/epgsql_geometry.hrl
  9. 50 0
      include/protocol.hrl
  10. 154 0
      pluggable_commands.md
  11. 94 0
      pluggable_types.md
  12. 17 1
      rebar.config
  13. 81 0
      src/commands/epgsql_cmd_batch.erl
  14. 40 0
      src/commands/epgsql_cmd_bind.erl
  15. 38 0
      src/commands/epgsql_cmd_close.erl
  16. 269 0
      src/commands/epgsql_cmd_connect.erl
  17. 39 0
      src/commands/epgsql_cmd_describe_portal.erl
  18. 63 0
      src/commands/epgsql_cmd_describe_statement.erl
  19. 85 0
      src/commands/epgsql_cmd_equery.erl
  20. 63 0
      src/commands/epgsql_cmd_execute.erl
  21. 72 0
      src/commands/epgsql_cmd_parse.erl
  22. 81 0
      src/commands/epgsql_cmd_prepared_query.erl
  23. 74 0
      src/commands/epgsql_cmd_squery.erl
  24. 68 0
      src/commands/epgsql_cmd_start_replication.erl
  25. 27 0
      src/commands/epgsql_cmd_sync.erl
  26. 61 0
      src/commands/epgsql_cmd_update_type_cache.erl
  27. 31 0
      src/datatypes/epgsql_codec_boolean.erl
  28. 39 0
      src/datatypes/epgsql_codec_bpchar.erl
  29. 59 0
      src/datatypes/epgsql_codec_datetime.erl
  30. 57 0
      src/datatypes/epgsql_codec_float.erl
  31. 32 0
      src/datatypes/epgsql_codec_geometric.erl
  32. 76 0
      src/datatypes/epgsql_codec_hstore.erl
  33. 40 0
      src/datatypes/epgsql_codec_integer.erl
  34. 90 0
      src/datatypes/epgsql_codec_intrange.erl
  35. 37 0
      src/datatypes/epgsql_codec_json.erl
  36. 74 0
      src/datatypes/epgsql_codec_net.erl
  37. 23 0
      src/datatypes/epgsql_codec_noop.erl
  38. 28 0
      src/datatypes/epgsql_codec_postgis.erl
  39. 51 0
      src/datatypes/epgsql_codec_text.erl
  40. 61 0
      src/datatypes/epgsql_codec_timerange.erl
  41. 37 0
      src/datatypes/epgsql_codec_uuid.erl
  42. 1 1
      src/epgsql.app.src
  43. 194 135
      src/epgsql.erl
  44. 299 320
      src/epgsql_binary.erl
  45. 81 0
      src/epgsql_codec.erl
  46. 65 0
      src/epgsql_command.erl
  47. 8 1
      src/epgsql_errcodes.erl
  48. 27 63
      src/epgsql_fdatetime.erl
  49. 37 34
      src/epgsql_idatetime.erl
  50. 195 0
      src/epgsql_oid_db.erl
  51. 11 0
      src/epgsql_replication.hrl
  52. 172 0
      src/epgsql_scram.erl
  53. 360 652
      src/epgsql_sock.erl
  54. 0 207
      src/epgsql_types.erl
  55. 124 82
      src/epgsql_wire.erl
  56. 49 41
      src/epgsqla.erl
  57. 41 34
      src/epgsqli.erl
  58. 79 36
      src/ewkb.erl
  59. 10 1
      test/data/test_schema.sql
  60. 204 55
      test/epgsql_SUITE.erl
  61. 10 22
      test/epgsql_cast.erl
  62. 19 0
      test/epgsql_codec_test_enum.erl
  63. 10 9
      test/epgsql_ct.erl
  64. 33 2
      test/epgsql_cth.erl
  65. 11 24
      test/epgsql_incremental.erl
  66. 1 2
      test/epgsql_replication_SUITE.erl

+ 5 - 7
.travis.yml

@@ -4,19 +4,17 @@ addons:
     packages:
       - postgresql-9.6-postgis-2.3
       - postgresql-contrib-9.6
-before_script:
-  - wget https://s3.amazonaws.com/rebar3/rebar3
-  - chmod u+x ./rebar3
 env:
   - PATH=".:/usr/lib/postgresql/9.6/bin:$PATH"
 install: "true"
 language: erlang
 otp_release:
-  - 20.0
-  - 19.1
+  - 21.0
+  - 20.3
+  - 19.3
   - 18.3
-  - 17.4
-  - R16B03-1
+  - 17.5
 script:
+  - make elvis
   - make test
   - make dialyzer

+ 73 - 0
CHANGES

@@ -1,3 +1,76 @@
+In 4.2.1
+
+* Bug fix for `epgsql:connect(proplist())`
+
+In 4.2.0
+
+* Support for R16 was completely dropped. Maps are now used internally instead of proplists
+* Nested `record` datatype decoding bug, introduced in 4.0.0, was fixed
+* Added tsrange, tstzrange, daterange types
+* Passwords are obfuscated before they sent to connection process. This is to avoid plain
+  DB passwords to be dumped to SASL crash log
+* Password can be provided as a fun: `fun ( () -> iodata() )`
+* `platform_define` macroses were simplified. It's now possible to build epgsql even without
+  rebar on all supported OTP versions
+
+In 4.1.0
+
+* Fix Erlang 21 `get_stacktrace/0` warnings
+* Fix broken backward-compatibility in `bpchar` datatype
+* Fix compatibility issues between the newest rebar3 and Erlang R16
+
+In 4.0.1
+
+* Minor build bug fixed
+
+In 4.0.0
+
+Changes:
+
+* Make epgsql commands pluggable  (see pluggable_commands.md).
+  Now we are not limited only by API functions provided by epgsql (equery/squery/prepared_query etc),
+  but may create own commands as a plugins. Still, understanding of PostgreSQL network protocol needed
+  to be able to do so.
+* Make epgsql datatype encoding/decdoding pluggable (see pluggable_types.md).
+  Now we are able to add or tweak codecs for existing as well as custom PostgreSQL
+  datatypes (like datetime, varchar, enum, arrays etc).
+  XXX: Highly recommend to add `{codecs, []}` option to your epgsql:connect/X calls if you don't use
+  PostGIS and hstore datatypes: this will reduce connection setup time.
+* epgsql internals had a huge refactoring (4,000 additions and 1,750 deletions for ~5500 LOC project).
+  Code now is much more modular, documented and have a lot of internal typespecs. Performance
+  improvements are also expected.
+* Now we try to use iolists as much as possible and avoid binary and string concatenations when ever possible.
+  Expect noticeable performance improvements for large BYTEA / TEXT / VARCHAR / JSON / JSONB etc fields.
+* Extended and documented epgsql:with_transaction/3.
+  Now it's possible to preserve original exception's stacktrace, finally!
+* macaddr and macaddr8 datatypes support added
+* Float datatype support was extended with `nan`, `minus_infinity`, `plus_infinity`
+* elvis code style check was added to our travis pipeline
+* Brand-new PostgreSQL 10 scram-sha-256 auth method added
+  (see https://www.postgresql.org/docs/10/static/auth-methods.html#AUTH-PASSWORD and
+  https://www.postgresql.org/docs/current/static/sasl-authentication.html)
+* A lot of typespecs were rewritten and lots of internal typespecs were added
+
+Incompatibilities:
+
+* Some unexpected performance issues may appear, but we expect performance improvements, especially
+  for a large result sets (when a lot of rows are returned by a query) and for large string/json/blob
+  query parameters
+* Undocumented epgsql:update_type_cache/2 API was changed
+* A lot of typespecs were updated. Some typespecs were deprecated or moved from epgsql.erl to other
+  modules. So, some new dialyzer warnings might pop-up
+* Some new error messages might be returned, especially from epgsql:connect/X functions
+* Memory consumption per-connection might slightly grow because we maintain per-connection OID<->codec
+  mapping table, which might be quite big in some cases. This also may produce bigger error_logger
+  reports in case of epgsql connection process crashes.
+* Some exported .hrl files have been changed. #column{} and #statement{} record definitions were extended.
+* PostGIS users might be affected by cleanup of ewkb.erl and epgsql_geometry.hrl
+* Streaming replication users should pay extra attention. No tests were broken, but a lot of
+  modifications were made to this code.
+* Passing integer / atom / float as a value of a text / varchar / bytea query parameter is now
+  deprecated (so, `epgsql:equery(C, "SELECT $1::text", [my_atom])` will still work but is not recommended)
+* Redshift and CockroachDB users might expirience some problems. Please, report bugs!
+
 In 3.4.0
 
 * Use rebar3 as default build and test tool

+ 11 - 4
Makefile

@@ -1,11 +1,15 @@
-REBAR = rebar3
+REBAR = ./rebar3
 
 all: compile
 
-compile: src/epgsql_errcodes.erl
+$(REBAR):
+	wget https://s3.amazonaws.com/rebar3/rebar3
+	chmod +x rebar3
+
+compile: src/epgsql_errcodes.erl $(REBAR)
 	@$(REBAR) compile
 
-clean:
+clean: $(REBAR)
 	@$(REBAR) clean
 
 src/epgsql_errcodes.erl:
@@ -17,4 +21,7 @@ test: compile
 dialyzer: compile
 	@$(REBAR) dialyzer
 
-.PHONY: all compile clean test dialyzer
+elvis: $(REBAR)
+	@$(REBAR) as lint lint
+
+.PHONY: all compile clean test dialyzer elvis

+ 116 - 71
README.md

@@ -57,51 +57,60 @@ see `CHANGES` for full list.
   (thousands of messages).
 
 ## Usage
+
 ### Connect
 
 ```erlang
--type host() :: inet:ip_address() | inet:hostname().
-
--type connect_option() ::
-    {database, DBName     :: string()}             |
-    {port,     PortNum    :: inet:port_number()}   |
-    {ssl,      IsEnabled  :: boolean() | required} |
-    {ssl_opts, SslOptions :: [ssl:ssl_option()]}   | % @see OTP ssl app, ssl_api.hrl
-    {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
-    {async,    Receiver   :: pid() | atom()}       | % process to receive LISTEN/NOTIFY msgs
-    {replication, Replication :: string()}. % Pass "database" to connect in replication mode
-    
--spec connect(host(), string(), string(), [connect_option()] | map())
-        -> {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 of extra options
-%% returns `{ok, Connection}' otherwise `{error, Reason}'
-connect(Host, Username, Password, Opts) -> ...
+connect(Opts) -> {ok, Connection :: epgsql:connection()} | {error, Reason :: epgsql:connect_error()}
+    when
+  Opts ::
+    #{host :=     inet:ip_address() | inet:hostname(),
+      username := iodata(),
+      password => iodata() | fun( () -> iodata() ),
+      database => iodata(),
+      port =>     inet:port_number(),
+      ssl =>      boolean() | required,
+      ssl_opts => [ssl:ssl_option()],    % @see OTP ssl app, ssl_api.hrl
+      timeout =>  timeout(),             % socket connect timeout, default: 5000 ms
+      async =>    pid() | atom(),        % process to receive LISTEN/NOTIFY msgs
+      codecs =>   [{epgsql_codec:codec_mod(), any()}]}
+      replication => Replication :: string()} % Pass "database" to connect in replication mode
+    | list().
+
+connect(Host, Username, Password, Opts) -> {ok, C} | {error, Reason}.
 ```
 example:
 ```erlang
-{ok, C} = epgsql:connect("localhost", "username", "psss", [
-    {database, "test_db"},
-    {timeout, 4000}
-]),
+{ok, C} = epgsql:connect("localhost", "username", "psss", #{
+    database => "test_db",
+    timeout => 4000
+}),
 ...
 ok = epgsql:close(C).
 ```
 
-The `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
-socket fails to connect within `TimeoutMs` milliseconds.
+Only `host` and `username` are mandatory, but most likely you would need `database` and `password`.
+
+- `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
+   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.
+- `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
+  is not available.
+- `ssl_opts` will be passed as is to `ssl:connect/3`
+- `async` see [Server notifications](#server-notifications)
+- `codecs` see [Pluggable datatype codecs](#pluggable-datatype-codecs)
+- `replication` see [Streaming replication protocol](#streaming-replication-protocol)
 
-Options may be passed as map with the same key names, if your VM version supports maps.
+Options may be passed as proplist or as map with the same key names.
 
 Asynchronous connect example (applies to **epgsqli** too):
 
 ```erlang
   {ok, C} = epgsqla:start_link(),
-  Ref = epgsqla:connect(C, "localhost", "username", "psss", [{database, "test_db"}]),
+  Ref = epgsqla:connect(C, "localhost", "username", "psss", #{database => "test_db"}),
   receive
     {C, Ref, connected} ->
         {ok, C};
@@ -139,49 +148,40 @@ squery(Connection, SqlQuery) -> ...
 ```
 examples:
 ```erlang
-InsertRes = epgsql:squery(C, "insert into account (name) values  ('alice'), ('bob')"),
-io:format("~p~n", [InsertRes]),
-```
-> ```
-{ok,2}
+epgsql:squery(C, "insert into account (name) values  ('alice'), ('bob')").
+> {ok,2}
 ```
 
 ```erlang
-SelectRes = epgsql:squery(C, "select * from account"),
-io:format("~p~n", [SelectRes]).
-```
-> ```
-{ok,
+epgsql:squery(C, "select * from account").
+> {ok,
     [{column,<<"id">>,int4,4,-1,0},{column,<<"name">>,text,-1,-1,0}],
     [{<<"1">>,<<"alice">>},{<<"2">>,<<"bob">>}]
 }
 ```
 
 ```erlang
-InsertReturningRes = epgsql:squery(C, 
+epgsql:squery(C,
     "insert into account(name)"
     "    values ('joe'), (null)"
-    "    returning *"),
-io:format("~p~n", [InsertReturningRes]).
-```
-> ```
-{ok,2,
+    "    returning *").
+> {ok,2,
     [{column,<<"id">>,int4,4,-1,0}, {column,<<"name">>,text,-1,-1,0}],
     [{<<"3">>,<<"joe">>},{<<"4">>,null}]
 }
 ```
 
 ```erlang
-{error, Reason} = epgsql:squery(C, "insert into account values (1, 'bad_pkey')"),
-io:format("~p~n", [Reason]).
-```
-> ```
-{error,
-    error,
-    <<"23505">>,
-    <<"duplicate key value violates unique constraint \"account_pkey\"">>,
-    [{detail,<<"Key (id)=(1) already exists.">>}]
-}
+-include_lib("epgsql/include/epgsql.hrl").
+epgsql:squery(C, "SELECT * FROM _nowhere_").
+> {error,
+   #error{severity = error,code = <<"42P01">>,
+          codename = undefined_table,
+          message = <<"relation \"_nowhere_\" does not exist">>,
+          extra = [{file,<<"parse_relation.c">>},
+                   {line,<<"1160">>},
+                   {position,<<"15">>},
+                   {routine,<<"parserOpenTable">>}]}}
 ```
 
 The simple query protocol returns all columns as binary strings
@@ -190,7 +190,7 @@ and does not support parameters binding.
 Several queries separated by semicolon can be executed by squery.
 
 ```erlang
-  [{ok, _, [{<<"1">>}]}, {ok, _, [{<<"2">>}]}] = epgsql:squery(C, "select 1; select 2").
+[{ok, _, [{<<"1">>}]}, {ok, _, [{<<"2">>}]}] = epgsql:squery(C, "select 1; select 2").
 ```
 
 `epgsqla:squery/2` returns result as a single message:
@@ -229,7 +229,7 @@ receive
 end.
 ```
 
-## Extended Query
+### Extended Query
 
 ```erlang
 {ok, Columns, Rows}        = epgsql:equery(C, "select ...", [Parameters]).
@@ -246,11 +246,8 @@ the unnamed prepared statement and portal. A `select` statement returns
 an error occurs, all statements result in `{error, #error{}}`.
 
 ```erlang
-SelectRes = epgsql:equery(C, "select id from account where name = $1", ["alice"]),
-io:format("~p~n", [SelectRes]).
-```
-> ```
-{ok,
+epgsql:equery(C, "select id from account where name = $1", ["alice"]),
+> {ok,
     [{column,<<"id">>,int4,4,-1,1}],
     [{1}]
 }
@@ -278,17 +275,17 @@ end.
 `epgsqli:equery(C, Statement, [TypedParameters])` sends same set of messages as
 squery including final `{C, Ref, done}`.
 
-## Prepared Query
+### Prepared Query
 ```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]).
-{error, Error}             = epgsql:prepared_equery(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 ...", []).```
 
-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.
 ```erlang
 epgsql:parse(C, "inc", "select $1+1", []).
@@ -313,7 +310,7 @@ end.
 `epgsqli:prepared_query(C, Statement, [TypedParameters])` sends same set of messages as
 squery including final `{C, Ref, done}`.
 
-## Parse/Bind/Execute
+### Parse/Bind/Execute
 
 ```erlang
 {ok, Statement} = epgsql:parse(C, [StatementName], Sql, [ParameterTypes]).
@@ -371,7 +368,7 @@ All epgsql functions return `{error, Error}` when an error occurs.
 `epgsqla`/`epgsqli` modules' `close` and `sync` functions send `{C, Ref, ok}`.
 
 
-## Batch execution
+### Batch execution
 
 Batch execution is `bind` + `execute` for several prepared statements.
 It uses unnamed portals and `MaxRows = 0`.
@@ -400,13 +397,17 @@ example:
 - `{C, Ref, done}` - execution of all queries from Batch has finished
 
 ## Data Representation
+
+Data representation may be configured using [pluggable datatype codecs](pluggable_types.md),
+so following is just default mapping:
+
 PG type       | Representation
 --------------|-------------------------------------
   null        | `null`
   bool        | `true` | `false`
   char        | `$A` | `binary`
   intX        | `1`
-  floatX      | `1.0`
+  floatX      | `1.0` | `nan` | `minus_infinity` | `plus_infinity`
   date        | `{Year, Month, Day}`
   time        | `{Hour, Minute, Second.Microsecond}`
   timetz      | `{time, Timezone}`
@@ -420,16 +421,26 @@ PG type       | Representation
   record      | `{int2, time, text, ...}` (decode only)
   point       |  `{10.2, 100.12}`
   int4range   | `[1,5)`
-  hstore      | `{list({binary(), binary() | null})}`
+  hstore      | `{[ {binary(), binary() \| null} ]}`
   json/jsonb  | `<<"{ \"key\": [ 1, 1.0, true, \"string\" ] }">>`
-
+  uuid        | `<<"123e4567-e89b-12d3-a456-426655440000">>`
+  inet        | `inet:ip_address()`
+  cidr        | `{ip_address(), Mask :: 0..32}`
+  macaddr(8)  | tuple of 6 or 8 `byte()`
+  geometry    | `ewkb:geometry()`
+  tsrange     | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
+  tstzrange   | `{{Hour, Minute, Second.Microsecond}, {Hour, Minute, Second.Microsecond}}`
+  daterange   | `{{Year, Month, Day}, {Year, Month, Day}}`
 
   `timestamp` and `timestamptz` parameters can take `erlang:now()` format: `{MegaSeconds, Seconds, MicroSeconds}`
 
-  `int4range` is a range type for ints (bigint not supported yet) that obeys inclusive/exclusive semantics,
+  `int4range` is a range type for ints that obeys inclusive/exclusive semantics,
   bracket and parentheses respectively. Additionally, infinities are represented by the atoms `minus_infinity`
   and `plus_infinity`
 
+  `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
+
 ## Errors
 
 Errors originating from the PostgreSQL backend are returned as `{error, #error{}}`,
@@ -485,6 +496,33 @@ Message formats:
 
 ## Utility functions
 
+### Transaction helpers
+
+```erlang
+with_transaction(connection(), fun((connection()) -> Result :: any()), Opts) ->
+    Result | {rollback, Reason :: any()} when
+Opts :: [{reraise, boolean()},
+         {ensure_committed, boolean()},
+         {begin_opts, iodata()}] | map().
+```
+
+Executes a function in a PostgreSQL transaction. It executes `BEGIN` prior to executing the function,
+`ROLLBACK` if the function raises an exception and `COMMIT` if the function returns without an error.
+If it is successful, it returns the result of the function. The failure case may differ, depending on
+the options passed.
+Options (proplist or map):
+- `reraise` (default `true`): when set to true, the original exception will be re-thrown after rollback,
+  otherwise `{rollback, ErrorReason}` will be returned
+- `ensure_committed` (default `false`): even when the callback returns without exception,
+  check that transaction was committed by checking the `CommandComplete` status
+  of the `COMMIT` command. If the transaction was rolled back, the status will be
+  `rollback` instead of `commit` and an `ensure_committed_failed` error will be generated.
+- `begin_opts` (default `""`): append extra options to `BEGIN` command (see
+  https://www.postgresql.org/docs/current/static/sql-begin.html) as a string by just
+  appending them to `"BEGIN "` string. Eg `{begin_opts, "ISOLATION LEVEL SERIALIZABLE"}`.
+  Beware of SQL injection! The value of `begin_opts` is not escaped!
+
+
 ### Command status
 
 `epgsql{a,i}:get_cmd_status(C) -> undefined | atom() | {atom(), integer()}`
@@ -516,6 +554,13 @@ Parameter's value may change during connection's lifetime.
 
 See [streaming.md](streaming.md).
 
+## Pluggable commands
+
+See [pluggable_commands.md](pluggable_commands.md)
+
+## Pluggable datatype codecs
+
+See [pluggable_types.md](pluggable_types.md)
 
 ## Mailing list
 

+ 1 - 1
generate_errcodes_src.sh

@@ -3,7 +3,7 @@
 # Used to generate epgsql_errcodes.erl
 #
 ERRFILE="https://raw.github.com/postgres/postgres/master/src/backend/utils/errcodes.txt"
-echo "%% DO NOT EDIT - AUTOGENERATED ON $(date)"
+echo "%% DO NOT EDIT - AUTOGENERATED BY $0 ON $(date +%Y-%m-%dT%H:%M:%S%z)"
 echo "-module(epgsql_errcodes)."
 echo "-export([to_name/1])."
 echo

+ 4 - 4
include/epgsql.hrl

@@ -1,8 +1,7 @@
--type epgsql_type() :: atom() | {array, atom()} | {unknown_oid, integer()}.
-
 -record(column, {
     name :: binary(),
-    type :: epgsql_type(),
+    type :: epgsql:epgsql_type(),
+    oid :: integer(),
     size :: -1 | pos_integer(),
     modifier :: -1 | pos_integer(),
     format :: integer()
@@ -11,7 +10,8 @@
 -record(statement, {
     name :: string(),
     columns :: [#column{}],
-    types :: [epgsql_type()]
+    types :: [epgsql:epgsql_type()],
+    parameter_info :: [epgsql_oid_db:oid_entry()]
 }).
 
 -record(error, {

+ 0 - 3
include/epgsql_binary.hrl

@@ -1,3 +0,0 @@
--define(int16, 1/big-signed-unit:16).
--define(int32, 1/big-signed-unit:32).
--define(int64, 1/big-signed-unit:64).

+ 13 - 61
include/epgsql_geometry.hrl

@@ -1,4 +1,3 @@
--type point_type() :: '2d' | '3d' | '2dm' | '3dm'.
 
 -record(point,{
   point_type :: any(),
@@ -8,114 +7,67 @@
   m :: float() | undefined
   }).
 
--type point(PointType) :: #point{ point_type :: PointType }.
-
 -record(multi_point,{
   point_type :: any(),
-  points :: [point(point_type())]
+  points :: [ewkb:point(ewkb:point_type())]
   }).
 
--type multi_point(PointType) :: #multi_point{ point_type :: PointType }.
-
 -record(line_string,{
   point_type :: any(),
-  points :: [point(point_type())]
+  points :: [ewkb:point(ewkb:point_type())]
   }).
 
--type line_string(PointType) :: #line_string{ point_type :: PointType }.
-
 -record(multi_line_string,{
   point_type :: any(),
-  line_strings :: [line_string(point_type())]
+  line_strings :: [ewkb:line_string(ewkb:point_type())]
   }).
 
--type multi_line_string(PointType) :: #multi_line_string{ point_type :: PointType }.
-
 -record(circular_string,{
   point_type :: any(),
-  points :: [point(point_type())]
+  points :: [ewkb:point(ewkb:point_type())]
   }).
 
--type basic_string(PointType) :: #circular_string{ point_type :: PointType } | #line_string{ point_type :: PointType }.
-
 -record(compound_curve,{
   point_type :: any(),
-  lines :: [basic_string(point_type())]
+  lines :: [ewkb:basic_string(ewkb:point_type())]
   }).
 
--type curve(PointType) :: #circular_string{ point_type :: PointType } | #line_string{ point_type :: PointType } | #compound_curve{ point_type :: PointType }.
-
 -record(multi_curve,{
   point_type :: any(),
-  curves :: [curve(point_type())]
+  curves :: [ewkb:curve(ewkb:point_type())]
   }).
 
--type multi_curve(PointType) :: #multi_curve{ point_type :: PointType }.
-
 -record(polygon,{
   point_type :: any(),
-  rings :: [line_string(point_type())]
+  rings :: [ewkb:line_string(ewkb:point_type())]
   }).
 
--type polygon(PointType) :: #polygon{ point_type :: PointType }.
-
 -record(multi_polygon,{
   point_type :: any(),
-  polygons :: [polygon(point_type())]
+  polygons :: [ewkb:polygon(ewkb:point_type())]
   }).
 
--type multi_polygon(PointType) :: #multi_polygon{ point_type :: PointType }.
-
 -record(triangle,{
   point_type :: any(),
-  rings :: [line_string(point_type())]
+  rings :: [ewkb:line_string(ewkb:point_type())]
   }).
 
--type triangle(PointType) :: #triangle{ point_type :: PointType }.
-
 -record(curve_polygon,{
   point_type :: any(),
-  rings :: [curve(point_type())]
+  rings :: [ewkb:curve(ewkb:point_type())]
   }).
 
--type curve_polygon(PointType) :: #curve_polygon{ point_type :: PointType }.
-
 -record(polyhedral_surface,{
   point_type :: any(),
-  polygons :: [polygon(point_type())]
+  polygons :: [ewkb:polygon(ewkb:point_type())]
   }).
 
--type polyhedral_surface(PointType) :: #polyhedral_surface{ point_type :: PointType }.
-
--type surface(PointType) :: polygon(PointType) | curve_polygon(PointType) | polyhedral_surface(PointType).
-
 -record(multi_surface,{
   point_type :: any(),
-  surfaces :: [surface(point_type())]
+  surfaces :: [ewkb:surface(ewkb:point_type())]
   }).
 
--type multi_surface(PointType) :: #multi_surface{ point_type :: PointType }.
-
 -record(tin,{
   point_type :: any(),
-  triangles :: [triangle(point_type())]
+  triangles :: [ewkb:triangle(ewkb:point_type())]
   }).
-
--type tin(PointType) :: #tin{ point_type :: PointType }.
-
--type geometry(PointType) :: point(PointType) |
-                             line_string(PointType) |
-                             triangle(PointType) |
-                             tin(PointType) |
-                             curve(PointType) |
-                             surface(PointType) |
-                             multi_point(PointType) |
-                             multi_line_string(PointType) |
-                             multi_polygon(PointType) |
-                             multi_curve(PointType) |
-                             multi_surface(PointType) |
-                             geometry_collection(PointType).
-
--type geometry() :: geometry(point_type()).
-
--type geometry_collection(PointType) :: [geometry(PointType)].

+ 50 - 0
include/protocol.hrl

@@ -0,0 +1,50 @@
+-define(int16, 1/big-signed-unit:16).
+-define(int32, 1/big-signed-unit:32).
+-define(int64, 1/big-signed-unit:64).
+
+%% Commands defined as per this page:
+%% https://www.postgresql.org/docs/current/static/protocol-message-formats.html
+
+%% Commands
+-define(BIND, $B).
+-define(CLOSE, $C).
+-define(DESCRIBE, $D).
+-define(EXECUTE, $E).
+-define(FLUSH, $H).
+-define(PASSWORD, $p).
+-define(PARSE, $P).
+-define(SIMPLEQUERY, $Q).
+-define(AUTHENTICATION_REQUEST, $R).
+-define(SYNC, $S).
+-define(SASL_ANY_RESPONSE, $p).
+
+%% Parameters
+
+-define(PREPARED_STATEMENT, $S).
+-define(PORTAL, $P).
+
+%% Responses
+
+-define(PARSE_COMPLETE, $1).
+-define(BIND_COMPLETE, $2).
+-define(CLOSE_COMPLETE, $3).
+-define(NOTIFICATION, $A).
+-define(COMMAND_COMPLETE, $C).
+-define(DATA_ROW, $D).
+-define(ERROR, $E).
+-define(EMPTY_QUERY, $I).
+-define(CANCELLATION_KEY, $K).
+-define(NO_DATA, $n).
+-define(NOTICE, $N).
+-define(PORTAL_SUSPENDED, $s).
+-define(PARAMETER_STATUS, $S).
+-define(PARAMETER_DESCRIPTION, $t).
+-define(ROW_DESCRIPTION, $T).
+-define(READY_FOR_QUERY, $Z).
+-define(COPY_BOTH_RESPONSE, $W).
+-define(COPY_DATA, $d).
+
+% CopyData replication messages
+-define(X_LOG_DATA, $w).
+-define(PRIMARY_KEEPALIVE_MESSAGE, $k).
+-define(STANDBY_STATUS_UPDATE, $r).

+ 154 - 0
pluggable_commands.md

@@ -0,0 +1,154 @@
+# Pluggable commands
+
+Starting from epgsql 4.0.0 it's possible to create custom epgsql commands. The term "command"
+signifies a single `request -> response` sequence.
+Under the hood it might contain many PostgreSQL protocol command requests and responses,
+but from the point of view of an epgsql user, it's a single request that produces a single
+response.
+Examples of such commands are `connect`, `squery`, `equery`, `prepared_query`,
+`parse`/`bind`/`execute` and so on. See [src/commands](src/commands) for a full list and
+examples. Basically, almost all epgsql end-user APIs are implemented as a commands.
+
+It is possible to send many commands without waiting for the results of previously sent ones
+(pipelining) by using `epgsqla` or `epgsqli` interfaces.
+
+## Possible usecases
+
+Why you may want to implement a custom command? Some ideas:
+
+* You are not satisfied by performance or functionality of epgsql's built-in commands
+* To create a version of equery with built-in statement cache
+* To create a single-roundtrip equery (currently equery works by combining `parse` and
+  `equery` commands)
+* To construct some tricky batching commands, eg, bulk-inserts
+
+## This can be done by following steps
+
+If you are not familiar with the PostgreSQL wire protocol, please, read at least the
+[Message Flow](https://www.postgresql.org/docs/current/static/protocol-flow.html) and
+[Message Formats](https://www.postgresql.org/docs/current/static/protocol-message-formats.html)
+sections of the PostgreSQL documentation.
+The entire [Frontend/Backend Protocol](https://www.postgresql.org/docs/current/static/protocol.html)
+would be nice to know.
+
+### Implement epgsql_command behaviour callback module
+
+See [epgsql_command](src/epgsql_command.erl).
+
+This module should have the following functions exported:
+
+```erlang
+init(any()) -> state().
+```
+
+Called only once when the command is received and is about to be executed by the epgsql connection
+process. Command's arguments are passed as the callback's arguments, see `epgsql_sock:sync_command/3` and
+`epgsql_sock:async_command/4`. Should initialize and return command's state that will be
+passed to all subsequent callbacks. No PostgreSQL interactions should be done here.
+
+```erlang
+execute(pg_sock(), state()) ->
+    {ok, pg_sock(), state()}
+  | {stop, Reason :: any(), Response :: any(), pg_sock()}.
+
+```
+
+Client -> Server packets should be sent from this callback by `epgsql_sock:send_multi/2` or
+`epgsql_sock:send/3`. `epgsql_wire` module is usually used to create wire protocol packets.
+Please note that many packets might be sent at once. See `epgsql_cmd_equery` as an example.
+
+This callback might be executed more than once for a single command execution if your command
+requires a response for some of the packets to send next packet (more than one round-trip).
+Since epgsql is asynchronous under the hood, you can't just do blocking `receive`.
+See `handle_message/4 -> {requeue, ...}` and `epgsql_cmd_connect` as an example.
+
+`pg_sock()` is an opaque state of a `epgsql_sock` process. There are some APIs to get or
+set some fields on it in `epgsql_sock` module.
+
+```erlang
+handle_message(Type :: byte(), Payload :: binary() | query_error(),
+               pg_sock(), state()) ->
+    {noaction, pg_sock()}
+  | {noaction, pg_sock(), state()}
+  | {add_row, tuple(), pg_sock(), state()}
+  | {add_result, Data :: any(), Notification :: any(), pg_sock(), state()}
+  | {finish, Result :: any(), Notification :: any(), pg_sock()}
+  | {requeue, pg_sock(), state()}
+  | {stop, Reason :: any(), Response :: any(), pg_sock()}
+  | {sync_required, Why :: any()}
+  | unknown.
+
+```
+
+Server -> Client packet handling code. Packet `Type` byte is the integer ID of a
+[protocol packet](https://www.postgresql.org/docs/current/static/protocol-message-formats.html), basically
+the 1st byte of a packet. And `Payload` is the remaining bytes of a packet. `epgsql_wire` module
+has some helpers that might help decode the packet payload.
+
+In the case when the epgsql connection gets an error packet from the server, it will be decoded and `Payload`
+will be `query_error()` instead of binary.
+
+**NEVER** call `epgsql_sock:send/3`/`epgsql_sock:send_multi/2` from this callback! Use
+`requeue` return instead: otherwise you will break pipelining!
+
+This callback should return one of the following responses to control command's behaviour:
+
+- `{noaction, pg_sock()}` - to do nothing (this usualy means that packet was ignored)
+- `{noaction, pg_sock(), state()}` - do nothing, but update command's state
+- `{add_row, tuple(), pg_sock(), state()}` - add a row to current resultset rows accumulator.
+  You may get the current accumulated resultset by `epgsql_sock::get_rows(pg_sock())` (except
+  when `epgsqli` interface is used).
+- `{add_result, Result :: any(), Notification :: any(), pg_sock(), state()}` - add a
+  new result to the list of results. Usualy all commands have only a single result, except `squery`, when
+  multiple SQL queries were passed, separated by a semicolon and `execute_batch`.
+  You will usually will just return something like `{ok, epgsql_sock:get_rows(PgSock)}` or an error as a result. `Notification` is used for `epgsqli` interface.
+  You may get the current list of accumulated results with `epgsql_sock:get_results(pg_sock())`.
+- `{finish, Results, Notification, pg_sock(), state()}` - returned when command was successfuly
+  executed and no more actions needed. `Results` will be returned to a client as a result of command
+  execution and the command will be descheduled from epgsql connection process.
+  You usually use the result of `epgsql_sock:get_results/1` as a `Results`.
+  `Notification` is used for `epgsqli` interface.
+- `{requeue, pg_sock(), state()}` - asks the epgsql process to put this command in the execution queue
+  once again (with a new state). This means that the `execute/2` callback will be executed again and
+  new packets may be sent from client to server. This way you can implement chatty commands with
+  multiple `request -> response` sequences. See `epgsql_cmd_connect` as an example.
+- `{stop, Reason, Response, pg_sock()}` - returned when some unrecoverable error occured and
+  you want to terminate epgsql connection process. `Response` will be returned as a command result
+  and `Reason` will be process termination reason.
+  Please, try to avoid use of this response if possible.
+- `{sync_required, Why}` - returned to finish command execution, flush enqueued but not yet
+  executed commands and to set epgsql process to `sync_required` state. In this state it
+  will not accept any commands except `epgsql_cmd_sync`.
+  This usualy means that multipacket protocol sequence was done out-of-order (eg, `bind` before `parse`),
+  so, client and server states are out-of-sync and we need to reset them.
+- `unknown` - command got unexpected packet. Connection process will be terminated with
+  `{error, {unexpected_message, Type, Payload, state()}}`. Usualy returned from a
+  catch-all last clause.
+
+### Command now can be executed
+
+By calling
+
+- `epgsql_sock:sync_command(connection(), command(), Args :: any())` for a
+  `gen_server:call`-style, synchronous behaviour (`epgsql`-like API)
+- `epgsql_sock:async_command(connection(), cast, command(), Args :: any())` for asynchronous
+  behaviour when whole resultset will be delivered as a single erlang message (`epgsqla`-like API)
+- `epgsql_sock:async_command(connection(), incremental, command(), Args :: any())` for
+  asynchronous behaviour when **each row** and some status info will be delivered as separate erlang
+  messages (`epgsqli`-like API)
+
+`command()` is the name of a module, implementing `epgsql_command` behaviour.
+`Args` may be any (eg, SQL query / arguments / options), they will be passed to `init/1` callback as-is.
+
+## Tips
+
+* If you are implementing your command outside of a epgsql main tree, it might be handy to
+  add `do(Conn, Arg1, Arg2...) -> epgsql_sock:sync_command(Conn, ?MODULE, Args).` to
+  incapsulate `epgsql_sock` calls and provide end-user API.
+* Don't be afraid of `requeue`. It might make your code more complex, but will make it possible to
+  implement complex multistep logic inside of a single command
+* `epgsql_sock` module has some APIs that might be used from within commands. Refer to that module's
+  source code. `epgsql_wire` has some helpers to encode/decode wire protocol and data packets.
+* Packet IDs are defined in `include/protocol.hrl`
+* Again, never try to send packets from `handle_message/4` or `init/1` callbacks!
+* Note that any error in callback functions will crash the epgsql connection process!

+ 94 - 0
pluggable_types.md

@@ -0,0 +1,94 @@
+# Pluggable types
+
+It's possible to make a custom datatype encoder/decoder as well as to change encoding/decoding
+of existing supported datatype.
+You can't have specific decoding rules for specific column or for specific query. Codec update
+affects any occurence of this datatype for this connection.
+
+## Possible usecases
+
+* Decode JSON inside epgsql
+* Change datetime representation
+* Add support for standard datatype that isn't supported by epgsql yet
+* Add support for contrib datatypes
+* Add codecs for your own custom datatypes (eg
+  [implemented on C level](https://www.postgresql.org/docs/current/static/xtypes.html) or
+  created by [CREATE TYPE](https://www.postgresql.org/docs/current/static/sql-createtype.html))
+
+## This can be done by following steps
+
+### Implement epgsql_codec behaviour callback module
+
+See [epgsql_codec](src/epgsql_codec.erl)
+
+This module should have following functions exported:
+
+```erlang
+init(any(), epgsql_sock:pg_sock()) -> codec_state().
+```
+Will be called only once on connection setup or when `update_type_cache/2` is called.
+Should initialize codec's internal state (if needed). This state will be passed as 1st
+argument to other callbacks later.
+
+```erlang
+names() -> [epgsql:type_name()].
+```
+Will be called immediately after init. It should return list of postgresql type names
+this codec is able to handle. Names should be the same as in column `typname` of `pg_type`
+table.
+
+```erlang
+encode(Data :: any(), epgsql:type_name(), codec_state()) -> iodata().
+```
+Will be called when parameter of matching type is passed to `equery` or `bind` etc.
+2nd argument is the name of matching type (useful when `names/0` returns more than one name).
+It should convert data to iolist / binary in a postgresql binary format representation.
+Postgresql binary format usualy not documented, so you most likely end up checking postgresql
+[source code](https://github.com/postgres/postgres/tree/master/src/backend/utils/adt).
+*TIP*: it's usualy more efficient to encode data as iolist, because in that case it will be
+written directly to socket without any extra copying. So, you don't need to call
+`iolist_to_binary/1` on your data before returning it from this function.
+
+```erlang
+decode(Data :: binary(), epgsql:type_name(), codec_state()) -> any()
+```
+If `equery` or `execute` returns a dataset that has columns of matching type, this function
+will be called for each "cell" of this type. It should parse postgresql binary format and
+return appropriate erlang representation.
+
+```erlang
+decode_text(Data :: binary(), epgsql:type_name(), codec_state()) -> any().
+```
+Optional callback. Will be called (if defined) in the same situation as `decode/3`, but for
+`squery` command results and data will be in postgresql text, not binary representation.
+By default epgsql will just return it as is.
+
+It would be nice to also define and export `in_data()`, `out_data()` and `data()` typespecs.
+
+Example: if your codec's `names/0` returns `[my_type, my_other_type]` and following command was
+executed:
+
+```erlang
+epgsql:equery(C, "SELECT $1::my_type, $1::my_type", [my_value])
+```
+
+Then `encode(my_value, my_type, codec_state())` will be called (only once). And, since we are doing select
+of a 2 values of type `my_type`, callback `decode(binary(), my_type, codec_state())` will be
+called 2 times.
+
+### Load this codec into epgsql
+
+It can be done by calling `epgsql:update_type_cache(Connection, [{CallbackModuleName, InitOptions}])` or
+by providing `{codecs, [{CallbackModuleName, InitOptions}]}` connect option.
+
+You may define new datatypes as well as redefine already supported ones.
+
+## Tips
+
+* When you just want to slightly change default decoding/encoding, it may be easier to emulate
+  inheritance by calling default codec's functions and then modifying what they return
+* Again, try to return iolists from `encode/3` when ever possible
+* You may pass options to `init/2`. It's the 2nd element of the tuple `{ModName, Opts}`.
+* You may use some context information from connection (it's internal record
+  passed as 2nd argument to `init/2`). See [epgsql_sock.erl](src/epgsql_sock.erl) for API functions.
+* Note that any error in callback functions will cause crash of epgsql connection process!

+ 17 - 1
rebar.config

@@ -1,4 +1,4 @@
-{erl_opts, [{platform_define, "^[0-9]+", have_maps}]}.
+{erl_opts, [{platform_define, "^17", 'SLOW_MAPS'}]}. % Erlang 17
 
 {eunit_opts, [verbose]}.
 
@@ -10,9 +10,25 @@
         {deps, [
             {erlexec, {git, "https://github.com/saleyn/erlexec.git", {ref, "576fb5d"}}}
         ]}
+    ]},
+    {lint, [
+        {plugins, [rebar3_lint]}
     ]}
 ]}.
 
 {ct_opts, [
     {ct_hooks, [epgsql_cth]}
 ]}.
+
+{elvis,
+  [#{dirs => ["src", "src/*"],
+     include_dirs => ["include"],
+     filter => "*.erl",
+     ruleset => erl_files,
+     rules =>
+         [{elvis_style, line_length, #{limit => 120}},
+          {elvis_style, god_modules, #{limit => 40}},
+          {elvis_style, state_record_and_type, disable} % epgsql_sock
+         ]}
+  ]
+ }.

+ 81 - 0
src/commands/epgsql_cmd_batch.erl

@@ -0,0 +1,81 @@
+%% > Bind
+%% < BindComplete
+%% > Execute
+%% < DataRow*
+%% < CommandComplete
+%% -- Repeated many times --
+%% > Sync
+%% < ReadyForQuery
+-module(epgsql_cmd_batch).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([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("protocol.hrl").
+
+-record(batch,
+        {batch :: [{#statement{}, list()}],
+         decoder}).
+
+init(Batch) ->
+    #batch{batch = Batch}.
+
+execute(Sock, #batch{batch = Batch} = State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Commands =
+        lists:foldr(
+          fun({Statement, Parameters}, Acc) ->
+                  #statement{name = StatementName,
+                             columns = Columns,
+                             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]
+          end,
+          [{?SYNC, []}],
+          Batch),
+    epgsql_sock:send_multi(Sock, Commands),
+    {ok, Sock, State}.
+
+handle_message(?BIND_COMPLETE, <<>>, Sock, #batch{batch = [{Stmt, _} | _]} = State) ->
+    #statement{columns = Columns} = Stmt,
+    Codec = epgsql_sock:get_codec(Sock),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    {noaction, Sock, State#batch{decoder = Decoder}};
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>, Sock,
+               #batch{decoder = Decoder} = State) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, State};
+%% handle_message(?EMPTY_QUERY, _, Sock, _State) ->
+%%     Sock1 = epgsql_sock:add_result(Sock, {complete, empty}, {ok, [], []}),
+%%     {noaction, Sock1};
+handle_message(?COMMAND_COMPLETE, Bin, Sock,
+               #batch{batch = [{#statement{columns = Columns}, _} | Batch]} = State) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    Rows = epgsql_sock:get_rows(Sock),
+    Result = case Complete of
+                 {_, Count} when Columns == [] ->
+                     {ok, Count};
+                 {_, Count} ->
+                     {ok, Count, Rows};
+                 _ ->
+                     {ok, Rows}
+             end,
+    {add_result, Result, {complete, Complete}, Sock, State#batch{batch = Batch}};
+handle_message(?READY_FOR_QUERY, _Status, Sock, #batch{batch = B} = _State) when
+      length(B) =< 1 ->
+    Results = epgsql_sock:get_results(Sock),
+    {finish, Results, done, Sock};
+handle_message(?ERROR, Error, Sock, #batch{batch = [_ | Batch]} = State) ->
+    Result = {error, Error},
+    {add_result, Result, Result, Sock, State#batch{batch = Batch}};
+handle_message(_, _, _, _) ->
+    unknown.

+ 40 - 0
src/commands/epgsql_cmd_bind.erl

@@ -0,0 +1,40 @@
+%% > Bind
+%% < BindComplete
+-module(epgsql_cmd_bind).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: ok | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-record(bind,
+        {stmt :: #statement{},
+         portal :: iodata(),
+         params :: list()}).
+
+init({Stmt, PortalName, Params}) ->
+    #bind{stmt = Stmt, portal = PortalName, params = Params}.
+
+execute(Sock, #bind{stmt = Stmt, portal = PortalName, params = Params} = St) ->
+    #statement{name = StatementName, columns = Columns, types = Types} = Stmt,
+    Codec = epgsql_sock:get_codec(Sock),
+    TypedParams = lists:zip(Types, Params),
+    Bin1 = epgsql_wire:encode_parameters(TypedParams, Codec),
+    Bin2 = epgsql_wire:encode_formats(Columns),
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?BIND, [PortalName, 0, StatementName, 0, Bin1, Bin2]},
+       {?FLUSH, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?BIND_COMPLETE, <<>>, Sock, _State) ->
+    {finish, ok, ok, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    {sync_required, {error, Error}};
+handle_message(_, _, _, _) ->
+    unknown.

+ 38 - 0
src/commands/epgsql_cmd_close.erl

@@ -0,0 +1,38 @@
+%% > Close
+%% < CloseComplete
+-module(epgsql_cmd_close).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: ok | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-record(close,
+        {type :: statement | portal,
+         name :: iodata()}).
+
+init({Type, Name}) ->
+    #close{type = Type, name = Name}.
+
+execute(Sock, #close{type = Type, name = Name} = St) ->
+    Type2 = case Type of
+        statement -> ?PREPARED_STATEMENT;
+        portal    -> ?PORTAL
+    end,
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?CLOSE, [Type2, Name, 0]},
+       {?FLUSH, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?CLOSE_COMPLETE, <<>>, Sock, _St) ->
+    {finish, ok, ok, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    {sync_required, {error, Error}};
+handle_message(_, _, _, _) ->
+    unknown.

+ 269 - 0
src/commands/epgsql_cmd_connect.erl

@@ -0,0 +1,269 @@
+%%% Special kind of command - it's exclusive: no other commands can run until
+%%% this one finishes.
+%%% It also uses some 'private' epgsql_sock's APIs
+%%%
+-module(epgsql_cmd_connect).
+-behaviour(epgsql_command).
+-export([hide_password/1, opts_hide_password/1]).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0, connect_error/0]).
+
+-type response() :: connected
+                  | {error, connect_error()}.
+-type connect_error() ::
+        invalid_authorization_specification
+      | invalid_password
+      | {unsupported_auth_method,
+         kerberosV5 | crypt | scm | gss | sspi | {unknown, integer()} | {sasl, [binary()]}}
+      | {sasl_server_final, any()}
+      | epgsql:query_error().
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-type auth_fun() :: fun((init | binary(), _, _) ->
+                                     {send, byte(), iodata(), any()}
+                                   | ok
+                                   | {error, any()}
+                                   | unknown).
+
+-record(connect,
+        {opts :: map(),
+         auth_fun :: auth_fun() | undefined,
+         auth_state :: any() | undefined,
+         auth_send :: {integer(), iodata()} | undefined,
+         stage = connect :: connect | maybe_auth | auth | initialization}).
+
+-define(SCRAM_AUTH_METHOD, <<"SCRAM-SHA-256">>).
+-define(AUTH_OK, 0).
+-define(AUTH_CLEARTEXT, 3).
+-define(AUTH_MD5, 5).
+-define(AUTH_SASL, 10).
+-define(AUTH_SASL_CONTINUE, 11).
+-define(AUTH_SASL_FINAL, 12).
+
+init(#{host := _, username := _} = Opts) ->
+    #connect{opts = Opts}.
+
+execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
+    #{host := Host,
+      username := Username} = Opts,
+    Timeout = maps:get(timeout, Opts, 5000),
+    Port = maps:get(port, Opts, 5432),
+    SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}, {keepalive, true}],
+    case gen_tcp:connect(Host, Port, SockOpts, Timeout) of
+        {ok, Sock} ->
+
+            %% Increase the buffer size.  Following the recommendation in the inet man page:
+            %%
+            %%    It is recommended to have val(buffer) >=
+            %%    max(val(sndbuf),val(recbuf)).
+
+            {ok, [{recbuf, RecBufSize}, {sndbuf, SndBufSize}]} =
+                inet:getopts(Sock, [recbuf, sndbuf]),
+            inet:setopts(Sock, [{buffer, max(RecBufSize, SndBufSize)}]),
+
+            PgSock1 = maybe_ssl(Sock, maps:get(ssl, Opts, false), Opts, PgSock),
+
+            Opts2 = ["user", 0, Username, 0],
+            Opts3 = case maps:find(database, Opts) of
+                error -> Opts2;
+                {ok, Database}  -> [Opts2 | ["database", 0, Database, 0]]
+            end,
+
+            {Opts4, PgSock2} =
+                case Opts of
+                    #{replication := Replication}  ->
+                        {[Opts3 | ["replication", 0, Replication, 0]],
+                         epgsql_sock:init_replication_state(PgSock1)};
+                        _ -> {Opts3, PgSock1}
+                    end,
+
+            epgsql_sock:send(PgSock2, [<<196608:?int32>>, Opts4, 0]),
+            PgSock3 = case Opts of
+                          #{async := Async} ->
+                              epgsql_sock:set_attr(async, Async, PgSock2);
+                          _ -> PgSock2
+                      end,
+            {ok, PgSock3, State#connect{stage = maybe_auth}};
+        {error, Reason} = Error ->
+            {stop, Reason, Error, PgSock}
+    end;
+execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
+    epgsql_sock:send(PgSock, PacketId, Data),
+    {ok, PgSock, St#connect{auth_send = undefined}}.
+
+
+%% @doc Replace `password' in Opts map with obfuscated one
+opts_hide_password(#{password := Password} = Opts) ->
+    HiddenPassword = hide_password(Password),
+    Opts#{password => HiddenPassword};
+opts_hide_password(Opts) -> Opts.
+
+
+%% @doc this function wraps plaintext password to a lambda function, so, if
+%% epgsql_sock process crashes when executing `connect` command, password will
+%% not appear in a crash log
+-spec hide_password(iodata()) -> fun( () -> iodata() ).
+hide_password(Password) when is_list(Password);
+                             is_binary(Password) ->
+    fun() ->
+            Password
+    end;
+hide_password(PasswordFun) when is_function(PasswordFun, 0) ->
+    PasswordFun.
+
+
+maybe_ssl(S, false, _, PgSock) ->
+    epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
+maybe_ssl(S, Flag, Opts, PgSock) ->
+    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  ->
+            SslOpts = maps:get(ssl_opts, Opts, []),
+            case ssl:connect(S, SslOpts, Timeout) of
+                {ok, S2}        ->
+                    epgsql_sock:set_net_socket(ssl, S2, PgSock);
+                {error, Reason} ->
+                    exit({ssl_negotiation_failed, Reason})
+            end;
+        $N ->
+            case Flag of
+                true ->
+                    epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
+                required ->
+                    exit(ssl_not_available)
+            end
+    end.
+
+%% Auth sub-protocol
+
+auth_init(<<?AUTH_CLEARTEXT:?int32>>, Sock, St) ->
+    auth_init(fun auth_cleartext/3, undefined, Sock, St);
+auth_init(<<?AUTH_MD5:?int32, Salt:4/binary>>, Sock, St) ->
+    auth_init(fun auth_md5/3, Salt, Sock, St);
+auth_init(<<?AUTH_SASL:?int32, MethodsB/binary>>, Sock, St) ->
+    Methods = epgsql_wire:decode_strings(MethodsB),
+    case lists:member(?SCRAM_AUTH_METHOD, Methods) of
+        true ->
+            auth_init(fun auth_scram/3, undefined, Sock, St);
+        false ->
+            {stop, normal, {error, {unsupported_auth_method,
+                                    {sasl, lists:delete(<<>>, Methods)}}}}
+    end;
+auth_init(<<M:?int32, _/binary>>, Sock, _St) ->
+    Method = case M of
+                 2 -> kerberosV5;
+                 4 -> crypt;
+                 6 -> scm;
+                 7 -> gss;
+                 8 -> sspi;
+                 _ -> {unknown, M}
+             end,
+    {stop, normal, {error, {unsupported_auth_method, Method}}, Sock}.
+
+auth_init(Fun, InitState, PgSock, St) ->
+    auth_handle(init, PgSock, St#connect{auth_fun = Fun, auth_state = InitState,
+                                         stage = auth}).
+
+auth_handle(Data, PgSock, #connect{auth_fun = Fun, auth_state = AuthSt} = St) ->
+    case Fun(Data, AuthSt, St) of
+        {send, SendPacketId, SendData, AuthSt1} ->
+            {requeue, PgSock, St#connect{auth_state = AuthSt1,
+                                         auth_send = {SendPacketId, SendData}}};
+        ok -> {noaction, PgSock, St};
+        {error, Reason} ->
+            {stop, normal, {error, Reason}};
+        unknown -> unknown
+    end.
+
+%% AuthenticationCleartextPassword
+auth_cleartext(init, _AuthState, #connect{opts = Opts}) ->
+    Password = get_password(Opts),
+    {send, ?PASSWORD, [Password, 0], undefined};
+auth_cleartext(_, _, _) -> unknown.
+
+%% AuthenticationMD5Password
+auth_md5(init, Salt, #connect{opts = Opts}) ->
+    User = maps:get(username, Opts),
+    Password = get_password(Opts),
+    Digest1 = hex(erlang:md5([Password, User])),
+    Str = ["md5", hex(erlang:md5([Digest1, Salt])), 0],
+    {send, ?PASSWORD, Str, undefined};
+auth_md5(_, _, _) -> unknown.
+
+%% AuthenticationSASL
+auth_scram(init, undefined, #connect{opts = Opts}) ->
+    User = maps:get(username, Opts),
+    Nonce = epgsql_scram:get_nonce(16),
+    ClientFirst = epgsql_scram:get_client_first(User, Nonce),
+    SaslInitialResponse = [?SCRAM_AUTH_METHOD, 0, <<(iolist_size(ClientFirst)):?int32>>, ClientFirst],
+    {send, ?SASL_ANY_RESPONSE, SaslInitialResponse, {auth_request, Nonce}};
+auth_scram(<<?AUTH_SASL_CONTINUE:?int32, ServerFirst/binary>>, {auth_request, Nonce}, #connect{opts = Opts}) ->
+    User = maps:get(username, Opts),
+    Password = get_password(Opts),
+    ServerFirstParts = epgsql_scram:parse_server_first(ServerFirst, Nonce),
+    {ClientFinalMessage, ServerProof} = epgsql_scram:get_client_final(ServerFirstParts, Nonce, User, Password),
+    {send, ?SASL_ANY_RESPONSE, ClientFinalMessage, {server_final, ServerProof}};
+auth_scram(<<?AUTH_SASL_FINAL:?int32, ServerFinalMsg/binary>>, {server_final, ServerProof}, _Conn) ->
+    case epgsql_scram:parse_server_final(ServerFinalMsg) of
+        {ok, ServerProof} -> ok;
+        Other -> {error, {sasl_server_final, Other}}
+    end;
+auth_scram(_, _, _) ->
+    unknown.
+
+
+%% --- Auth ---
+
+%% AuthenticationOk
+handle_message(?AUTHENTICATION_REQUEST, <<?AUTH_OK:?int32>>, Sock, State) ->
+    {noaction, Sock, State#connect{stage = initialization,
+                                   auth_fun = undefined,
+                                   auth_state = undefned,
+                                   auth_send = undefined}};
+
+handle_message(?AUTHENTICATION_REQUEST, Message, Sock, #connect{stage = Stage} = St) when Stage =/= auth ->
+    auth_init(Message, Sock, St);
+
+handle_message(?AUTHENTICATION_REQUEST, Packet, Sock, #connect{stage = auth} = St) ->
+    auth_handle(Packet, Sock, St);
+
+%% --- Initialization ---
+
+%% BackendKeyData
+handle_message(?CANCELLATION_KEY, <<Pid:?int32, Key:?int32>>, Sock, _State) ->
+    {noaction, epgsql_sock:set_attr(backend, {Pid, Key}, Sock)};
+
+%% ReadyForQuery
+handle_message(?READY_FOR_QUERY, _, Sock, _State) ->
+    Codec = epgsql_binary:new_codec(Sock, []),
+    Sock1 = epgsql_sock:set_attr(codec, Codec, Sock),
+    {finish, connected, connected, Sock1};
+
+
+%% ErrorResponse
+handle_message(?ERROR, Err, Sock, #connect{stage = Stage} = _State) when Stage == auth;
+                                                                         Stage == maybe_auth ->
+    Why = case Err#error.code of
+        <<"28000">> -> invalid_authorization_specification;
+        <<"28P01">> -> invalid_password;
+        Any         -> Any
+    end,
+    {stop, normal, {error, Why}, Sock};
+handle_message(_, _, _, _) ->
+    unknown.
+
+
+get_password(Opts) ->
+    PasswordFun = maps:get(password, Opts),
+    PasswordFun().
+
+
+hex(Bin) ->
+    HChar = fun(N) when N < 10 -> $0 + N;
+               (N) when N < 16 -> $W + N
+            end,
+    <<<<(HChar(H)), (HChar(L))>> || <<H:4, L:4>> <= Bin>>.

+ 39 - 0
src/commands/epgsql_cmd_describe_portal.erl

@@ -0,0 +1,39 @@
+%% > Describe
+%% < RowDescription | NoData
+-module(epgsql_cmd_describe_portal).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-type response() :: {ok, [epgsql:column()]} | {error, epgsql:query_error()}.
+
+-record(desc_portal,
+        {name :: iodata(),
+         parameter_descr}).
+
+init(Name) ->
+    #desc_portal{name = Name}.
+
+execute(Sock, #desc_portal{name = Name} = St) ->
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?DESCRIBE, [?PORTAL, Name, 0]},
+       {?FLUSH, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock, St) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
+    {finish, {ok, Columns}, {columns, Columns}, St};
+handle_message(?NO_DATA, <<>>, _Sock, _State) ->
+    {finish, {ok, []}, no_data};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    Result = {error, Error},
+    {sync_required, Result};
+handle_message(_, _, _, _) ->
+    unknown.

+ 63 - 0
src/commands/epgsql_cmd_describe_statement.erl

@@ -0,0 +1,63 @@
+%% Almost the same as "parse"
+%% > Describe
+%% < ParameterDescription
+%% < RowDescription | NoData
+-module(epgsql_cmd_describe_statement).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-type response() :: {ok, #statement{}} | {error, epgsql:query_error()}.
+
+-record(desc_stmt,
+        {name :: iodata(),
+         parameter_typenames = [],
+         parameter_descr = []}).
+
+init(Name) ->
+    #desc_stmt{name = Name}.
+
+execute(Sock, #desc_stmt{name = Name} = St) ->
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?DESCRIBE, [?PREPARED_STATEMENT, Name, 0]},
+       {?FLUSH, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?PARAMETER_DESCRIPTION, Bin, Sock, State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    TypeInfos = epgsql_wire:decode_parameters(Bin, Codec),
+    OidInfos = [epgsql_binary:typeinfo_to_oid_info(Type, Codec) || Type <- TypeInfos],
+    TypeNames = [epgsql_binary:typeinfo_to_name_array(Type, Codec) || Type <- TypeInfos],
+    Sock2 = epgsql_sock:notify(Sock, {types, TypeNames}),
+    {noaction, Sock2, State#desc_stmt{parameter_descr = OidInfos,
+                                      parameter_typenames = TypeNames}};
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock,
+               #desc_stmt{name = Name, parameter_descr = Params,
+                          parameter_typenames = TypeNames}) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
+    Columns2 = [Col#column{format = epgsql_wire:format(Col, Codec)}
+                || Col <- Columns],
+    Result = {ok, #statement{name = Name,
+                             types = TypeNames,
+                             parameter_info = Params,
+                             columns = Columns2}},
+    {finish, Result, {columns, Columns2}, Sock};
+handle_message(?NO_DATA, <<>>, Sock, #desc_stmt{name = Name, parameter_descr = Params,
+                                                parameter_typenames = TypeNames}) ->
+    Result = {ok, #statement{name = Name,
+                             types = TypeNames,
+                             parameter_info = Params,
+                             columns = []}},
+    {finish, Result, no_data, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    Result = {error, Error},
+    {sync_required, Result};
+handle_message(_, _, _, _) ->
+    unknown.

+ 85 - 0
src/commands/epgsql_cmd_equery.erl

@@ -0,0 +1,85 @@
+%% > Bind
+%% < BindComplete
+%% > Execute
+%% < DataRow*
+%% < CommandComplete
+%% > Close
+%% < CloseComplete
+%% > Sync
+%% < ReadyForQuery
+-module(epgsql_cmd_equery).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: {ok, Count :: non_neg_integer(), Cols :: [epgsql:column()], Rows :: [tuple()]}
+                  | {ok, Count :: non_neg_integer()}
+                  | {ok, Cols :: [epgsql:column()], Rows :: [tuple()]}
+                  | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-record(equery,
+        {stmt :: #statement{},
+         params :: list(),
+         decoder}).
+
+init({Stmt, TypedParams}) ->
+    #equery{stmt = Stmt,
+            params = TypedParams}.
+
+execute(Sock, #equery{stmt = Stmt, params = TypedParams} = St) ->
+    #statement{name = StatementName, columns = Columns} = Stmt,
+    Codec = epgsql_sock:get_codec(Sock),
+    Bin1 = epgsql_wire:encode_parameters(TypedParams, Codec),
+    Bin2 = epgsql_wire:encode_formats(Columns),
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?BIND, ["", 0, StatementName, 0, Bin1, Bin2]},
+       {?EXECUTE, ["", 0, <<0:?int32>>]},
+       {?CLOSE, [?PREPARED_STATEMENT, StatementName, 0]},
+       {?SYNC, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?BIND_COMPLETE, <<>>, Sock, #equery{stmt = Stmt} = State) ->
+    #statement{columns = Columns} = Stmt,
+    epgsql_sock:notify(Sock, {columns, Columns}), % Why do we need this?
+    Codec = epgsql_sock:get_codec(Sock),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    {noaction, Sock, State#equery{decoder = Decoder}};
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>,
+               Sock, #equery{decoder = Decoder} = St) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, St};
+handle_message(?EMPTY_QUERY, <<>>, Sock, St) ->
+    {add_result, {ok, [], []}, {complete, empty}, Sock, St};
+handle_message(?COMMAND_COMPLETE, Bin, Sock, #equery{stmt = Stmt} = St) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    #statement{columns = Cols} = Stmt,
+    Rows = epgsql_sock:get_rows(Sock),
+    Result = case Complete of
+                 {_, Count} when Cols == [] ->
+                     {ok, Count};
+                 {_, Count} ->
+                     {ok, Count, Cols, Rows};
+                 _ ->
+                     {ok, Cols, Rows}
+             end,
+    {add_result, Result, {complete, Complete}, Sock, St};
+handle_message(?CLOSE_COMPLETE, _, Sock, _State) ->
+    {noaction, Sock};
+handle_message(?READY_FOR_QUERY, _Status, Sock, _State) ->
+    case epgsql_sock:get_results(Sock) of
+        [Result] ->
+            {finish, Result, done, Sock};
+        [] ->
+            {finish, done, done, Sock}
+    end;
+handle_message(?ERROR, Error, Sock, St) ->
+    Result = {error, Error},
+    {add_result, Result, Result, Sock, St};
+handle_message(_, _, _, _) ->
+    unknown.

+ 63 - 0
src/commands/epgsql_cmd_execute.erl

@@ -0,0 +1,63 @@
+%% > Execute
+%% < DataRow*
+%% < CommandComplete | PortalSuspended
+-module(epgsql_cmd_execute).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: {ok, Count :: non_neg_integer(), Rows :: [tuple()]}
+                  | {ok, Count :: non_neg_integer()}
+                  | {ok | partial, Rows :: [tuple()]}
+                  | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-record(execute,
+        {stmt :: #statement{},
+         portal_name :: iodata(),
+         max_rows :: non_neg_integer(),
+         decoder}).
+
+init({Stmt, PortalName, MaxRows}) ->
+    #execute{stmt = Stmt, portal_name = PortalName, max_rows = MaxRows}.
+
+execute(Sock, #execute{stmt = Stmt, portal_name = PortalName, max_rows = MaxRows} = State) ->
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?EXECUTE, [PortalName, 0, <<MaxRows:?int32>>]},
+       {?FLUSH, []}
+      ]),
+    #statement{columns = Columns} = Stmt,
+    Codec = epgsql_sock:get_codec(Sock),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    {ok, Sock, State#execute{decoder = Decoder}}.
+
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>, Sock,
+               #execute{decoder = Decoder} = St) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, St};
+handle_message(?EMPTY_QUERY, _, Sock, _State) ->
+    {finish, {ok, [], []}, {complete, empty}, Sock};
+handle_message(?COMMAND_COMPLETE, Bin, Sock,
+               #execute{stmt = #statement{columns = Cols}}) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    Rows = epgsql_sock:get_rows(Sock),
+    Result = case Complete of
+                 {_, Count} when Cols == [] ->
+                     {ok, Count};
+                 {_, Count} ->
+                     {ok, Count, Rows};
+                 _ ->
+                     {ok, Rows}
+             end,
+    {finish, Result, {complete, Complete}, Sock};
+handle_message(?PORTAL_SUSPENDED, <<>>, Sock, _State) ->
+    Rows = epgsql_sock:get_rows(Sock),
+    {finish, {partial, Rows}, suspended, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    {sync_required, {error, Error}};
+handle_message(_, _, _, _) ->
+    unknown.

+ 72 - 0
src/commands/epgsql_cmd_parse.erl

@@ -0,0 +1,72 @@
+%% > Parse
+%% < ParseComplete
+%% > Describe
+%% < ParameterDescription
+%% < RowDescription | NoData
+-module(epgsql_cmd_parse).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-type response() :: {ok, #statement{}} | {error, epgsql:query_error()}.
+
+-record(parse,
+        {name :: iodata(),
+         sql :: iodata(),
+         types :: [atom()],
+         parameter_typenames = [] :: [epgsql:type_name() | {array, epgsql:type_name()}],
+         parameter_descr = [] :: [epgsql_oid_db:oid_info()]}).
+
+%% FIXME: make it use oids instead of type names!
+init({Name, Sql, Types}) ->
+    #parse{name = Name, sql = Sql, types = Types}.
+
+execute(Sock, #parse{name = Name, sql = Sql, types = Types} = St) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Bin = epgsql_wire:encode_types(Types, Codec),
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?PARSE, [Name, 0, Sql, 0, Bin]},
+       {?DESCRIBE, [?PREPARED_STATEMENT, Name, 0]},
+       {?FLUSH, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?PARSE_COMPLETE, <<>>, Sock, _State) ->
+    {noaction, Sock};
+handle_message(?PARAMETER_DESCRIPTION, Bin, Sock, State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    TypeInfos = epgsql_wire:decode_parameters(Bin, Codec),
+    OidInfos = [epgsql_binary:typeinfo_to_oid_info(Type, Codec) || Type <- TypeInfos],
+    TypeNames = [epgsql_binary:typeinfo_to_name_array(Type, Codec) || Type <- TypeInfos],
+    Sock2 = epgsql_sock:notify(Sock, {types, TypeNames}),
+    {noaction, Sock2, State#parse{parameter_descr = OidInfos,
+                                  parameter_typenames = TypeNames}};
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock,
+               #parse{name = Name, parameter_descr = Params,
+                      parameter_typenames = TypeNames}) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
+    Columns2 = [Col#column{format = epgsql_wire:format(Col, Codec)}
+                || Col <- Columns],
+    Result = {ok, #statement{name = Name,
+                             types = TypeNames,
+                             columns = Columns2,
+                             parameter_info = Params}},
+    {finish, Result, {columns, Columns2}, Sock};
+handle_message(?NO_DATA, <<>>, Sock, #parse{name = Name, parameter_descr = Params,
+                                            parameter_typenames = TypeNames}) ->
+    Result = {ok, #statement{name = Name,
+                             types = TypeNames,
+                             parameter_info = Params,
+                             columns = []}},
+    {finish, Result, no_data, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    Result = {error, Error},
+    {sync_required, Result};
+handle_message(_, _, _, _) ->
+    unknown.

+ 81 - 0
src/commands/epgsql_cmd_prepared_query.erl

@@ -0,0 +1,81 @@
+%% Almost the same as equery, but don't execute 'CLOSE'
+%% > Bind
+%% < BindComplete
+%% > Execute
+%% < DataRow*
+%% < CommandComplete
+%% > Sync
+%% < ReadyForQuery
+-module(epgsql_cmd_prepared_query).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: {ok, Count :: non_neg_integer(), Cols :: [epgsql:column()], Rows :: [tuple()]}
+                  | {ok, Count :: non_neg_integer()}
+                  | {ok, Cols :: [epgsql:column()], Rows :: [tuple()]}
+                  | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+-record(pquery,
+        {stmt :: #statement{},
+         params :: list(),
+         decoder}).
+
+init({Stmt, TypedParams}) ->
+    #pquery{stmt = Stmt,
+            params = TypedParams}.
+
+execute(Sock, #pquery{stmt = Stmt, params = TypedParams} = St) ->
+    #statement{name = StatementName, columns = Columns} = Stmt,
+    Codec = epgsql_sock:get_codec(Sock),
+    Bin1 = epgsql_wire:encode_parameters(TypedParams, Codec),
+    Bin2 = epgsql_wire:encode_formats(Columns),
+    epgsql_sock:send_multi(
+      Sock,
+      [
+       {?BIND, ["", 0, StatementName, 0, Bin1, Bin2]},
+       {?EXECUTE, ["", 0, <<0:?int32>>]},
+       {?SYNC, []}
+      ]),
+    {ok, Sock, St}.
+
+handle_message(?BIND_COMPLETE, <<>>, Sock, #pquery{stmt = Stmt} = State) ->
+    #statement{columns = Columns} = Stmt,
+    epgsql_sock:notify(Sock, {columns, Columns}), % Why do we need this?
+    Codec = epgsql_sock:get_codec(Sock),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    {noaction, Sock, State#pquery{decoder = Decoder}};
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>,
+               Sock, #pquery{decoder = Decoder} = St) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, St};
+handle_message(?EMPTY_QUERY, _, Sock, St) ->
+    {add_result, {ok, [], []}, {complete, empty}, Sock, St};
+handle_message(?COMMAND_COMPLETE, Bin, Sock, #pquery{stmt = Stmt} = St) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    #statement{columns = Cols} = Stmt,
+    Rows = epgsql_sock:get_rows(Sock),
+    Result = case Complete of
+                 {_, Count} when Cols == [] ->
+                     {ok, Count};
+                 {_, Count} ->
+                     {ok, Count, Cols, Rows};
+                 _ ->
+                     {ok, Cols, Rows}
+             end,
+    {add_result, Result, {complete, Complete}, Sock, St};
+handle_message(?READY_FOR_QUERY, _Status, Sock, _State) ->
+    case epgsql_sock:get_results(Sock) of
+        [Result] ->
+            {finish, Result, done, Sock};
+        [] ->
+            {finish, done, done, Sock}
+    end;
+handle_message(?ERROR, Error, Sock, St) ->
+    Result = {error, Error},
+    {add_result, Result, Result, Sock, St};
+handle_message(_, _, _, _) ->
+    unknown.

+ 74 - 0
src/commands/epgsql_cmd_squery.erl

@@ -0,0 +1,74 @@
+%% Squery may contain many semicolon-separated queries
+%% > Query
+%% < (RowDescription?
+%% <  DataRow*
+%% <  CommandComplete)+
+%% < ReadyForQuery
+%% ---
+%% > Query when len(strip(Query)) == 0
+%% < EmptyQueryResponse
+%% < ReadyForQuery
+-module(epgsql_cmd_squery).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response_single() ::
+        {ok, Count :: non_neg_integer(), Cols :: [epgsql:column()], Rows :: [tuple()]}
+      | {ok, Count :: non_neg_integer()}
+      | {ok, Cols :: [epgsql:column()], Rows :: [tuple()]}
+      | {error, epgsql:query_error()}.
+-type response() :: response_single() | [response_single()].
+
+-include("protocol.hrl").
+
+-record(squery,
+        {query :: iodata(),
+         columns = [],
+         decoder}).
+
+init(Sql) ->
+    #squery{query = Sql}.
+
+execute(Sock, #squery{query = Q} = State) ->
+    epgsql_sock:send(Sock, ?SIMPLEQUERY, [Q, 0]),
+    {ok, Sock, State}.
+
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock, State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    epgsql_sock:notify(Sock, {columns, Columns}),
+    {noaction, Sock, State#squery{columns = Columns,
+                                  decoder = Decoder}};
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>,
+               Sock, #squery{decoder = Decoder} = St) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, St};
+handle_message(?COMMAND_COMPLETE, Bin, Sock, #squery{columns = Cols} = St) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    Rows = epgsql_sock:get_rows(Sock),
+    Result = case Complete of
+                 {_, Count} when Cols == [] ->
+                     {ok, Count};
+                 {_, Count} ->
+                     {ok, Count, Cols, Rows};
+                 _ ->
+                     {ok, Cols, Rows}
+             end,
+    {add_result, Result, {complete, Complete}, Sock, St};
+handle_message(?EMPTY_QUERY, _, Sock, St) ->
+    {add_result, {ok, [], []}, {complete, empty}, Sock, St};
+handle_message(?READY_FOR_QUERY, _Status, Sock, _State) ->
+    %% We return single result if there is only one or list of results if
+    %% there are more than one
+    Result = case epgsql_sock:get_results(Sock) of
+                 [Res] -> Res;
+                 Res -> Res
+             end,
+    {finish, Result, done, Sock};
+handle_message(?ERROR, Error, Sock, St) ->
+    Result = {error, Error},
+    {add_result, Result, Result, Sock, St};
+handle_message(_, _, _, _) ->
+    unknown.

+ 68 - 0
src/commands/epgsql_cmd_start_replication.erl

@@ -0,0 +1,68 @@
+%% > SimpleQuery "START_REPLICATION ..."
+%% < CopyBothResponse | Error
+-module(epgsql_cmd_start_replication).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: ok | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+-include("../epgsql_replication.hrl").
+
+-record(start_repl,
+        {slot,
+         callback,
+         cb_state,
+         wal_pos,
+         plugin_opts,
+         opts}).
+
+init({ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts}) ->
+    #start_repl{slot = ReplicationSlot,
+                callback = Callback,
+                cb_state = CbInitState,
+                wal_pos = WALPosition,
+                plugin_opts = PluginOpts,
+                opts = Opts}.
+
+execute(Sock, #start_repl{slot = ReplicationSlot, callback = Callback,
+                          cb_state = CbInitState, wal_pos = WALPosition,
+                          plugin_opts = PluginOpts, opts = Opts} = St) ->
+    %% Connection should be started with 'replication' option. Then
+    %% 'replication_state' will be initialized
+    Repl = #repl{} = epgsql_sock:get_replication_state(Sock),
+    Sql1 = ["START_REPLICATION SLOT ", ReplicationSlot, " LOGICAL ", WALPosition],
+    Sql2 =
+        case PluginOpts of
+            [] -> Sql1;
+            PluginOpts -> [Sql1 , " (", PluginOpts, ")"]
+        end,
+
+    Repl2 =
+        case Callback of
+            Pid when is_pid(Pid) -> Repl#repl{receiver = Pid};
+            Module -> Repl#repl{cbmodule = Module, cbstate = CbInitState}
+        end,
+
+    Hex = [H || H <- WALPosition, H =/= $/],
+    {ok, [LSN], _} = io_lib:fread("~16u", Hex),
+    AlignLsn = proplists:get_value(align_lsn, Opts, false),
+    Repl3 = Repl2#repl{last_flushed_lsn = LSN,
+                       last_applied_lsn = LSN,
+                       align_lsn = AlignLsn},
+    Sock2 = epgsql_sock:set_attr(replication_state, Repl3, Sock),
+                         %% handler = on_replication},
+
+    epgsql_sock:send(Sock2, ?SIMPLEQUERY, [Sql2, 0]),
+    {ok, Sock2, St}.
+
+%% CopyBothResponse
+handle_message(?COPY_BOTH_RESPONSE, _Data, Sock, _State) ->
+    {finish, ok, ok, epgsql_sock:set_packet_handler(on_replication, Sock)};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    Result = {error, Error},
+    {sync_required, Result};
+handle_message(_, _, _, _) ->
+    unknown.

+ 27 - 0
src/commands/epgsql_cmd_sync.erl

@@ -0,0 +1,27 @@
+%% > Sync
+%% < ReadyForQuery
+-module(epgsql_cmd_sync).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() :: ok | {error, epgsql:query_error()}.
+
+-include("epgsql.hrl").
+-include("protocol.hrl").
+
+
+init(_) ->
+    undefined.
+
+execute(Sock, St) ->
+    epgsql_sock:send(Sock, ?SYNC, []),
+    Sock1 = epgsql_sock:set_attr(sync_required, false, Sock),
+    {ok, Sock1, St}.
+
+handle_message(?READY_FOR_QUERY, _, Sock, _State) ->
+    {finish, ok, ok, Sock};
+handle_message(?ERROR, Error, _Sock, _State) ->
+    {sync_required, {error, Error}};
+handle_message(_, _, _, _) ->
+    unknown.

+ 61 - 0
src/commands/epgsql_cmd_update_type_cache.erl

@@ -0,0 +1,61 @@
+%% Special command. Executes Squery over pg_type table and updates codecs.
+-module(epgsql_cmd_update_type_cache).
+-behaviour(epgsql_command).
+-export([init/1, execute/2, handle_message/4]).
+-export_type([response/0]).
+
+-type response() ::
+        {ok, [epgsql:type_name()]}
+      | {error, epgsql:query_error()}.
+
+-include("protocol.hrl").
+
+-record(upd,
+        {codecs :: [{epgsql_codec:codec_mod(), Opts :: any()}],
+         codec_entries :: [epgsql_codec:codec_entry()] | undefined,
+         decoder :: epgsql_wire:row_decoder() | undefined}).
+
+init(Codecs) ->
+    #upd{codecs = Codecs}.
+
+execute(Sock, #upd{codecs = Codecs} = State) ->
+    CodecEntries = epgsql_codec:init_mods(Codecs, Sock),
+    TypeNames = [element(1, Entry) || Entry <- CodecEntries],
+    Query = epgsql_oid_db:build_query(TypeNames),
+    epgsql_sock:send(Sock, ?SIMPLEQUERY, [Query, 0]),
+    {ok, Sock, State#upd{codec_entries = CodecEntries}}.
+
+handle_message(?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>, Sock, State) ->
+    Codec = epgsql_sock:get_codec(Sock),
+    Columns = epgsql_wire:decode_columns(Count, Bin, Codec),
+    Decoder = epgsql_wire:build_decoder(Columns, Codec),
+    {noaction, Sock, State#upd{decoder = Decoder}};
+handle_message(?DATA_ROW, <<_Count:?int16, Bin/binary>>,
+               Sock, #upd{decoder = Decoder} = St) ->
+    Row = epgsql_wire:decode_data(Bin, Decoder),
+    {add_row, Row, Sock, St};
+handle_message(?COMMAND_COMPLETE, Bin, Sock, St) ->
+    Complete = epgsql_wire:decode_complete(Bin),
+    Rows = epgsql_sock:get_rows(Sock),
+    {add_result, Rows, {complete, Complete}, Sock, St};
+handle_message(?READY_FOR_QUERY, _Status, Sock, State) ->
+    [Result] = epgsql_sock:get_results(Sock),
+    handle_result(Result, Sock, State);
+handle_message(?ERROR, Error, Sock, St) ->
+    Result = {error, Error},
+    {add_result, Result, Result, Sock, St};
+handle_message(_, _, _, _) ->
+    unknown.
+
+handle_result({error, _} = Err, Sock, _State) ->
+    {finish, Err, done, Sock};
+handle_result(Rows, Sock, #upd{codec_entries = CodecEntries} = _State) ->
+    OidEntries = epgsql_oid_db:parse_rows(Rows),
+    Types = epgsql_oid_db:join_codecs_oids(OidEntries, CodecEntries),
+
+    Codec = epgsql_sock:get_codec(Sock),
+    Codec1 = epgsql_binary:update_codec(Types, Codec),
+    Sock1 = epgsql_sock:set_attr(codec, Codec1, Sock),
+
+    TypeNames = [element(1, Entry) || Entry <- CodecEntries],
+    {finish, {ok, TypeNames}, done, Sock1}.

+ 31 - 0
src/datatypes/epgsql_codec_boolean.erl

@@ -0,0 +1,31 @@
+%%% @doc
+%%% Codec for `bool'.
+%%% `unknown' is represented by `null'.
+%%% https://www.postgresql.org/docs/current/static/datatype-boolean.html
+%%% $PG$/src/backend/utils/adt/bool.c
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_boolean).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: boolean().
+
+init(_, _) -> [].
+
+names() ->
+    [bool].
+
+encode(true, bool, _) ->
+    <<1:1/big-signed-unit:8>>;
+encode(false, bool, _) ->
+    <<0:1/big-signed-unit:8>>.
+
+decode(<<1:1/big-signed-unit:8>>, bool, _) -> true;
+decode(<<0:1/big-signed-unit:8>>, bool, _) -> false.
+
+decode_text(V, _, _) -> V.

+ 39 - 0
src/datatypes/epgsql_codec_bpchar.erl

@@ -0,0 +1,39 @@
+%%% @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
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_bpchar).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: binary() | byte().
+
+init(_, _) -> [].
+
+names() ->
+    [bpchar, char].
+
+encode(C, _, _) when is_integer(C), C =< 255 ->
+    <<C:1/big-unsigned-unit:8>>;
+encode(Bin, bpchar, _) when is_binary(Bin) ->
+    Bin;
+encode(Str, bpchar, _) when is_list(Str) ->
+    %% See epgsql_codec_text:encode/3
+    try iolist_size(Str) of
+        _ -> Str
+    catch error:badarg ->
+            unicode:characters_to_binary(Str)
+    end.
+
+decode(<<C:1/big-unsigned-unit:8>>, _, _) -> C;
+decode(Bin, bpchar, _) -> Bin.
+
+decode_text(V, _, _) -> V.

+ 59 - 0
src/datatypes/epgsql_codec_datetime.erl

@@ -0,0 +1,59 @@
+%%% @doc
+%%% 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'
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_datetime).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+-export_type([pg_date/0,
+              pg_time/0,
+              pg_datetime/0,
+              pg_interval/0,
+              pg_timetz/0]).
+
+-type data() :: pg_date() | pg_time() | pg_datetime() | pg_interval() | pg_timetz().
+
+%% Ranges are from https://www.postgresql.org/docs/current/static/datatype-datetime.html
+-type pg_date() ::
+        {Year :: -4712..294276,
+         Month :: 1..12,
+         Day :: 1..31}.
+-type pg_time() ::
+        {Hour :: 0..24,  % Max value is 24:00:00
+         Minute :: 0..59,
+         Second :: 0..59 | float()}.
+-type pg_timetz() :: {pg_time(), UtcOffset :: integer()}.
+-type pg_datetime() :: {pg_date(), pg_time()}.
+-type pg_interval() :: {pg_time(), Days :: integer(), Months :: integer()}.
+
+
+init(_, Sock) ->
+    case epgsql_sock:get_parameter_internal(<<"integer_datetimes">>, Sock) of
+        <<"on">>  -> epgsql_idatetime;
+        <<"off">> -> epgsql_fdatetime
+    end.
+
+names() ->
+    [time, timetz, date, timestamp, timestamptz, interval].
+
+%% FIXME: move common logic out from fdatetime/idatetime; make them more
+%% low-level
+encode(Val, Type, epgsql_idatetime) ->
+    epgsql_idatetime:encode(Type, Val);
+encode(Val, Type, epgsql_fdatetime) ->
+    epgsql_fdatetime:encode(Type, Val).
+
+decode(Bin, Type, epgsql_idatetime) ->
+    epgsql_idatetime:decode(Type, Bin);
+decode(Bin, Type, epgsql_fdatetime) ->
+    epgsql_fdatetime:decode(Type, Bin).
+
+decode_text(V, _, _) -> V.

+ 57 - 0
src/datatypes/epgsql_codec_float.erl

@@ -0,0 +1,57 @@
+%%% @doc
+%%% 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
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_float).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: in_data() | out_data().
+-type in_data() :: integer() | float() | nan | plus_infinity | minus_infinity.
+-type out_data() :: float() | nan | plus_infinity | minus_infinity.
+
+-define(POS_INF,  <<0:1, 255:8, 0:23>>).
+-define(NEG_INF,  <<1:1, 255:8, 0:23>>).
+-define(NAN_PATTERN, <<_:1, 255:8, _:23>>).
+-define(NAN, <<0:1, 255:8, 1:1, 0:22>>).
+
+-define(POS_INF8, <<0:1, 2047:11, 0:52>>).
+-define(NEG_INF8, <<1:1, 2047:11, 0:52>>).
+-define(NAN_PATTERN8, <<_:1, 2047:11, _:52>>).
+-define(NAN8, <<0:1, 2047:11, 1:1, 0:51>>).
+
+init(_, _) -> [].
+
+names() ->
+    [float4, float8].
+
+encode(Int, Type, State) when is_integer(Int) ->
+    encode(Int * 1.0, Type, State);
+encode(N, float4, _) when is_float(N) ->
+    <<N:1/big-float-unit:32>>;
+encode(N, float8, _) when is_float(N) ->
+    <<N:1/big-float-unit:64>>;
+encode(nan, float4, _) -> ?NAN;
+encode(nan, float8, _) -> ?NAN8;
+encode(plus_infinity, float4, _) -> ?POS_INF;
+encode(plus_infinity, float8, _) -> ?POS_INF8;
+encode(minus_infinity, float4, _) -> ?NEG_INF;
+encode(minus_infinity, float8, _) -> ?NEG_INF8.
+
+
+decode(<<N:1/big-float-unit:32>>, float4, _) -> N;
+decode(<<N:1/big-float-unit:64>>, float8, _) -> N;
+decode(?POS_INF, float4, _) -> plus_infinity;
+decode(?POS_INF8, float8, _) -> plus_infinity;
+decode(?NEG_INF, float4, _) -> minus_infinity;
+decode(?NEG_INF8, float8, _) -> minus_infinity;
+decode(?NAN_PATTERN, float4, _) -> nan;
+decode(?NAN_PATTERN8, float8, _) -> nan.
+
+decode_text(V, _, _) -> V.

+ 32 - 0
src/datatypes/epgsql_codec_geometric.erl

@@ -0,0 +1,32 @@
+%%% @doc
+%%% 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!
+%%% @end
+%%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+%%% TODO: line, lseg, box, path, polygon, circle
+
+-module(epgsql_codec_geometric).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: point().
+-type point() :: {float(), float()}.
+
+init(_, _) -> [].
+
+names() ->
+    [point].
+
+encode({X, Y}, point, _) when is_number(X), is_number(Y) ->
+    %% XXX: looks like it doesn't have size prefix?!
+    <<X:1/big-float-unit:64, Y:1/big-float-unit:64>>.
+
+decode(<<X:1/big-float-unit:64, Y:1/big-float-unit:64>>, point, _) ->
+    {X, Y}.
+
+decode_text(V, _, _) -> V.

+ 76 - 0
src/datatypes/epgsql_codec_hstore.erl

@@ -0,0 +1,76 @@
+%%% @doc
+%%% 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/
+%%% @end
+%%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_hstore).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-include("protocol.hrl").
+
+-export_type([data/0]).
+
+-type data() :: data_in() | data_out().
+
+-type key_in() :: list() | binary() | atom() | integer() | float().
+%% jiffy-style maps
+-type data_in() :: { [{key_in(), binary()}] }.
+-type data_out() :: { [{Key :: binary(), Value :: binary()}] }.
+
+-dialyzer([{nowarn_function, [encode/3]}, no_improper_lists]).
+
+%% TODO: option for output format: proplist | jiffy-object | map
+init(_, _) -> [].
+
+names() ->
+    [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].
+
+decode(<<Size:?int32, Elements/binary>>, hstore, _) ->
+    {do_decode(Size, Elements)}.
+
+
+encode_key(K) ->
+    encode_string(K).
+
+encode_value(null) ->
+    <<-1:?int32>>;
+encode_value(undefined) ->
+    <<-1:?int32>>;
+encode_value(V) ->
+    encode_string(V).
+
+encode_string(Str) when is_binary(Str) ->
+    <<(byte_size(Str)):?int32, Str/binary>>;
+encode_string(Str) when is_list(Str) ->
+    encode_string(list_to_binary(Str));
+encode_string(Str) when is_atom(Str) ->
+    encode_string(atom_to_binary(Str, utf8));
+encode_string(Str) when is_integer(Str) ->
+    encode_string(integer_to_binary(Str));
+encode_string(Str) when is_float(Str) ->
+    encode_string(io_lib:format("~w", [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(N, <<KeyLen:?int32, Key:KeyLen/binary,
+               ValLen:?int32, Value:ValLen/binary, Rest/binary>>) ->
+    [{Key, Value} | do_decode(N - 1, Rest)].
+
+decode_text(V, _, _) -> V.

+ 40 - 0
src/datatypes/epgsql_codec_integer.erl

@@ -0,0 +1,40 @@
+%%% @doc
+%%% 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
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_integer).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+%% See table 8.2
+%% https://www.postgresql.org/docs/current/static/datatype-numeric.html
+-define(BIGINT_MAX, 16#7fffffffffffffff).  % 9223372036854775807, (2^63 - 1)
+-define(BIGINT_MIN, -16#7fffffffffffffff). % -9223372036854775807
+
+-type data() :: ?BIGINT_MIN..?BIGINT_MAX.
+
+
+init(_, _) -> [].
+
+names() ->
+    [int2, int4, int8].
+
+encode(N, int2, _) ->
+    <<N:1/big-signed-unit:16>>;
+encode(N, int4, _) ->
+    <<N:1/big-signed-unit:32>>;
+encode(N, int8, _) ->
+    <<N:1/big-signed-unit:64>>.
+
+decode(<<N:1/big-signed-unit:16>>, int2, _)    -> N;
+decode(<<N:1/big-signed-unit:32>>, int4, _)    -> N;
+decode(<<N:1/big-signed-unit:64>>, int8, _)    -> N.
+
+decode_text(V, _, _) -> V.

+ 90 - 0
src/datatypes/epgsql_codec_intrange.erl

@@ -0,0 +1,90 @@
+%%% @doc
+%%% Codec for `int4range', `int8range' types.
+%%% https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin
+%%% $PG$/src/backend/utils/adt/rangetypes.c
+%%% @end
+%%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+%%% TODO: universal range, based on pg_range table
+%%% TODO: inclusive/exclusive ranges `[]' `[)' `(]' `()'
+
+-module(epgsql_codec_intrange).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-include("protocol.hrl").
+
+-export_type([data/0]).
+
+-type data() :: {left(), right()}.
+
+-type left() :: minus_infinity | integer().
+-type right() :: plus_infinity | integer().
+
+
+init(_, _) -> [].
+
+names() ->
+    [int4range, int8range].
+
+encode(Range, int4range, _) ->
+    encode_int4range(Range);
+encode(Range, int8range, _) ->
+    encode_int8range(Range).
+
+decode(Bin, int4range, _) ->
+    decode_int4range(Bin);
+decode(Bin, int8range, _) ->
+    decode_int8range(Bin).
+
+
+encode_int4range({minus_infinity, plus_infinity}) ->
+    <<24:1/big-signed-unit:8>>;
+encode_int4range({From, plus_infinity}) ->
+    FromInt = to_int(From),
+    <<18:1/big-signed-unit:8, 4:?int32, FromInt:?int32>>;
+encode_int4range({minus_infinity, To}) ->
+    ToInt = to_int(To),
+    <<8:1/big-signed-unit:8, 4:?int32, ToInt:?int32>>;
+encode_int4range({From, To}) ->
+    FromInt = to_int(From),
+    ToInt = to_int(To),
+    <<2:1/big-signed-unit:8, 4:?int32, FromInt:?int32, 4:?int32, ToInt:?int32>>.
+
+encode_int8range({minus_infinity, plus_infinity}) ->
+    <<24:1/big-signed-unit:8>>;
+encode_int8range({From, plus_infinity}) ->
+    FromInt = to_int(From),
+    <<18:1/big-signed-unit:8, 8:?int32, FromInt:?int64>>;
+encode_int8range({minus_infinity, To}) ->
+    ToInt = to_int(To),
+    <<8:1/big-signed-unit:8, 8:?int32, ToInt:?int64>>;
+encode_int8range({From, To}) ->
+    FromInt = to_int(From),
+    ToInt = to_int(To),
+    <<2:1/big-signed-unit:8, 8:?int32, FromInt:?int64, 8:?int32, ToInt:?int64>>.
+
+to_int(N) when is_integer(N) -> N;
+to_int(S) when is_list(S) -> erlang:list_to_integer(S);
+to_int(B) when is_binary(B) -> erlang:binary_to_integer(B).
+
+
+decode_int4range(<<2:1/big-signed-unit:8, 4:?int32, From:?int32, 4:?int32, To:?int32>>) ->
+    {From, To};
+decode_int4range(<<8:1/big-signed-unit:8, 4:?int32, To:?int32>>) ->
+    {minus_infinity, To};
+decode_int4range(<<18:1/big-signed-unit:8, 4:?int32, From:?int32>>) ->
+    {From, plus_infinity};
+decode_int4range(<<24:1/big-signed-unit:8>>) ->
+    {minus_infinity, plus_infinity}.
+
+decode_int8range(<<2:1/big-signed-unit:8, 8:?int32, From:?int64, 8:?int32, To:?int64>>) ->
+    {From, To};
+decode_int8range(<<8:1/big-signed-unit:8, 8:?int32, To:?int64>>) ->
+    {minus_infinity, To};
+decode_int8range(<<18:1/big-signed-unit:8, 8:?int32, From:?int64>>) ->
+    {From, plus_infinity};
+decode_int8range(<<24:1/big-signed-unit:8>>) ->
+    {minus_infinity, plus_infinity}.
+
+decode_text(V, _, _) -> V.

+ 37 - 0
src/datatypes/epgsql_codec_json.erl

@@ -0,0 +1,37 @@
+%%% @doc
+%%% 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'
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_json).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: binary().
+
+-define(JSONB_VERSION_1, 1).
+
+%% TODO: JSON encode/decode `fun Mod:Name/1` / `{Mod, Name}` as option.
+%% Shall not pass `fun(_) -> .. end`, because of hot code upgrade problems.
+init(_, _) -> [].
+
+names() ->
+    [json, jsonb].
+
+encode(Bin, json, _) ->
+    Bin;
+encode(Bin, jsonb, _) ->
+    [<<?JSONB_VERSION_1:8>> | Bin].
+
+decode(Bin, json, _) ->
+    Bin;
+decode(<<?JSONB_VERSION_1:8, Bin/binary>>, jsonb, _) ->
+    Bin.
+
+decode_text(V, _, _) -> V.

+ 74 - 0
src/datatypes/epgsql_codec_net.erl

@@ -0,0 +1,74 @@
+%%% @doc
+%%% 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.
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+-module(epgsql_codec_net).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: ip() | ip_mask() | macaddr() | macaddr8().
+
+-type ip() :: inet:ip_address().
+-type mask() :: 0..32.
+-type ip_mask() :: {ip(), mask()}.
+-type macaddr() :: {byte(), byte(), byte(), byte(), byte(), byte()}.
+-type macaddr8() :: {byte(), byte(), byte(), byte(), byte(), byte(), byte(), byte()}.
+
+-define(INET, 2).
+-define(INET6, 3).
+-define(IP_SIZE, 4).
+-define(IP6_SIZE, 16).
+-define(MAX_IP_MASK, 32).
+-define(MAX_IP6_MASK, 128).
+
+init(_, _) -> [].
+
+names() ->
+    [inet, cidr, macaddr, macaddr8].
+
+encode({B1, B2, B3, B4, B5, B6}, macaddr, _) ->
+    <<B1, B2, B3, B4, B5, B6>>;
+encode({B1, B2, B3, B4, B5, B6, B7, B8}, macaddr8, _) ->
+    <<B1, B2, B3, B4, B5, B6, B7, B8>>;
+encode(IpMask, _, _) ->
+    encode_net(IpMask).
+
+decode(<<B1, B2, B3, B4, B5, B6>>, macaddr, _) ->
+    {B1, B2, B3, B4, B5, B6};
+decode(<<B1, B2, B3, B4, B5, B6, B7, B8>>, macaddr8, _) ->
+    {B1, B2, B3, B4, B5, B6, B7, B8};
+decode(Bin, _, _) ->
+    decode_net(Bin).
+
+-spec encode_net(ip() | ip_mask()) -> binary().
+encode_net({{_, _, _, _} = IP, Mask}) ->
+    Bin = list_to_binary(tuple_to_list(IP)),
+    <<?INET, Mask:8, 1, ?IP_SIZE, Bin/binary>>;
+encode_net({{_, _, _, _, _, _, _, _} = IP, Mask}) ->
+    Bin = << <<X:16>> || X <- tuple_to_list(IP) >>,
+    <<?INET6, Mask:8, 1, ?IP6_SIZE, Bin/binary>>;
+encode_net({_, _, _, _} = IP) ->
+    Bin = list_to_binary(tuple_to_list(IP)),
+    <<?INET, ?MAX_IP_MASK, 0, ?IP_SIZE, Bin/binary>>;
+encode_net({_, _, _, _, _, _, _, _} = IP) ->
+    Bin = << <<X:16>> || X <- tuple_to_list(IP) >>,
+    <<?INET6, ?MAX_IP6_MASK, 0, ?IP6_SIZE, Bin/binary>>.
+
+-spec decode_net(binary()) -> ip() | ip_mask().
+decode_net(<<?INET, Mask:8, 1, ?IP_SIZE, Bin/binary>>) ->
+    {list_to_tuple(binary_to_list(Bin)), Mask};
+decode_net(<<?INET6, Mask:8, 1, ?IP6_SIZE, Bin/binary>>) ->
+    {list_to_tuple([X || <<X:16>> <= Bin]), Mask};
+decode_net(<<?INET, ?MAX_IP_MASK, 0, ?IP_SIZE, Bin/binary>>) ->
+    list_to_tuple(binary_to_list(Bin));
+decode_net(<<?INET6, ?MAX_IP6_MASK, 0, ?IP6_SIZE, Bin/binary>>) ->
+    list_to_tuple([X || <<X:16>> <= Bin]).
+
+decode_text(V, _, _) -> V.

+ 23 - 0
src/datatypes/epgsql_codec_noop.erl

@@ -0,0 +1,23 @@
+%%% @doc
+%%% Dummy codec. Used internally
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_noop).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: binary().
+
+init(_, _) -> [].
+
+names() -> [].
+
+encode(Bin, _, _) when is_binary(Bin) -> Bin.
+
+decode(Bin, _, _) -> Bin.
+
+decode_text(Bin, _, _) -> Bin.

+ 28 - 0
src/datatypes/epgsql_codec_postgis.erl

@@ -0,0 +1,28 @@
+%%% @doc
+%%% Codec for `geometry' PostGIS umbrella datatype.
+%%% http://postgis.net/docs/manual-2.4/geometry.html
+%%% $POSTGIS$/postgis/lwgeom_inout.c
+%%% @end
+%%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_postgis).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: ewkb:geometry().
+
+init(_, _) -> [].
+
+names() ->
+    [geometry].
+
+encode(Geo, geometry, _) ->
+    ewkb:encode_geometry(Geo).
+
+decode(Bin, geometry, _) ->
+    ewkb:decode_geometry(Bin).
+
+decode_text(V, _, _) -> V.

+ 51 - 0
src/datatypes/epgsql_codec_text.erl

@@ -0,0 +1,51 @@
+%%% @doc
+%%% 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
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_text).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: in_data() | out_data().
+-type in_data() :: binary() | iolist() | string().
+-type out_data() :: binary().
+
+init(_, _) -> [].
+
+names() ->
+    [text, varchar, bytea].
+
+encode(String, Name, State) when is_list(String) ->
+    %% Try to not convert iolists to binary; this way they will be written directly to socket
+    %% But we are doing implicit check that iolist is well formed by calling
+    %% relatively cheap iolist_size/1 on it
+    try iolist_size(String) of
+        _ -> String
+    catch error:badarg when Name == varchar orelse Name == text ->
+            %% Maybe it's a unicode string; try to convert it to bytes
+            encode(unicode:characters_to_binary(String), Name, State)
+    end;
+encode(Bin, _, _) when is_binary(Bin) -> Bin;
+encode(Other, _Name, _State) ->
+    %% This is for backward compatibitlty! Maybe add warning?
+    %% error_logger:warning_msg(
+    %%   "epgsql_codec_text.erl: Deprecated attempt to encode '~p' as '~s'",
+    %%   [Other, Name]),
+    encode_compat(Other).
+
+encode_compat(A) when is_atom(A)    -> atom_to_binary(A, utf8);
+encode_compat(I) when is_integer(I) -> integer_to_binary(I);
+encode_compat(F) when is_float(F)   -> float_to_binary(F).
+
+
+decode(Bin, _, _) -> Bin.
+
+decode_text(V, _, _) -> V.

+ 61 - 0
src/datatypes/epgsql_codec_timerange.erl

@@ -0,0 +1,61 @@
+%%% @doc
+%%% Codec for `tsrange', `tstzrange', `daterange' types.
+%%% https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-builtin
+%%% $PG$/src/backend/utils/adt/rangetypes.c
+%%% @end
+%%% Created : 16 Jul 2018 by Vladimir Sekissov <eryx67@gmail.com>
+%%% TODO: universal range, based on pg_range table
+%%% TODO: inclusive/exclusive ranges `[]' `[)' `(]' `()'
+
+-module(epgsql_codec_timerange).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-include("protocol.hrl").
+
+-export_type([data/0]).
+
+-type data() :: {epgsql_codec_datetime:data(), epgsql_codec_datetime:data()} | empty.
+
+init(_, Sock) ->
+    case epgsql_sock:get_parameter_internal(<<"integer_datetimes">>, Sock) of
+        <<"on">>  -> epgsql_idatetime;
+        <<"off">> -> epgsql_fdatetime
+    end.
+
+names() ->
+    [tsrange, tstzrange, daterange].
+
+encode(empty, _T, _CM) ->
+    <<1>>;
+encode({From, To}, Type, EncMod) ->
+    FromBin = encode_member(Type, From, EncMod),
+    ToBin = encode_member(Type, To, EncMod),
+    <<2:1/big-signed-unit:8,
+      (byte_size(FromBin)):?int32, FromBin/binary,
+      (byte_size(ToBin)):?int32, ToBin/binary>>.
+
+decode(<<1>>, _, _) ->
+    empty;
+decode(<<2:1/big-signed-unit:8,
+         FromLen:?int32, FromBin:FromLen/binary,
+         ToLen:?int32, ToBin:ToLen/binary>>,
+       Type, EncMod) ->
+    {decode_member(Type, FromBin, EncMod), decode_member(Type, ToBin, EncMod)}.
+
+decode_text(V, _, _) -> V.
+
+encode_member(Type, Val, epgsql_idatetime) ->
+    epgsql_idatetime:encode(member_type(Type), Val);
+encode_member(Type, Val, epgsql_fdatetime) ->
+    epgsql_fdatetime:encode(member_type(Type), Val).
+
+decode_member(Type, Bin, epgsql_idatetime) ->
+    epgsql_idatetime:decode(member_type(Type), Bin);
+decode_member(Type, Bin, epgsql_fdatetime) ->
+    epgsql_fdatetime:decode(member_type(Type), Bin).
+
+member_type(tsrange) -> timestamp;
+member_type(tstzrange) -> timestamptz;
+member_type(daterange) -> date.

+ 37 - 0
src/datatypes/epgsql_codec_uuid.erl

@@ -0,0 +1,37 @@
+%%% @doc
+%%% Codec for `uuid' type.
+%%% Input expected to be in hex string, eg
+%%% `<<"550e8400-e29b-41d4-a716-446655440000">>'.
+%%% https://www.postgresql.org/docs/current/static/datatype-uuid.html
+%%% $PG$/src/backend/utils/adt/uuid.c
+%%% @end
+%%% Created : 14 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec_uuid).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3, decode_text/3]).
+
+-export_type([data/0]).
+
+-type data() :: in_data() | out_data().
+-type in_data() :: string() | binary().
+-type out_data() :: binary().
+
+init(_, _) -> [].
+
+names() ->
+    [uuid].
+
+encode(Uuid, uuid, St) when is_list(Uuid) ->
+    encode(list_to_binary(Uuid), uuid, St);
+encode(Uuid, uuid, _) when is_binary(Uuid) ->
+    Hex = binary:replace(Uuid, <<"-">>, <<>>, [global]),
+    Int = erlang:binary_to_integer(Hex, 16),
+    <<Int:128/big-unsigned-integer>>.
+
+decode(<<U0:32, U1:16, U2:16, U3:16, U4:48>>, uuid, _) ->
+    Format = "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",
+    iolist_to_binary(io_lib:format(Format, [U0, U1, U2, U3, U4])).
+
+decode_text(V, _, _) -> V.

+ 1 - 1
src/epgsql.app.src

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

+ 194 - 135
src/epgsql.erl

@@ -22,22 +22,27 @@
          update_type_cache/1,
          update_type_cache/2,
          with_transaction/2,
+         with_transaction/3,
          sync_on_error/2,
          standby_status_update/3,
          start_replication/5,
          start_replication/6,
          start_replication/7,
-         to_proplist/1]).
+         to_map/1]).
+-export([handle_x_log_data/5]).                 % private
 
 -export_type([connection/0, connect_option/0, connect_opts/0,
-              connect_error/0, query_error/0,
-              sql_query/0, bind_param/0, typed_param/0,
+              connect_error/0, query_error/0, sql_query/0, column/0,
+              type_name/0, epgsql_type/0, statement/0]).
+
+%% Deprecated types
+-export_type([bind_param/0, typed_param/0,
               squery_row/0, equery_row/0, reply/1,
               pg_time/0, pg_date/0, pg_datetime/0, pg_interval/0]).
 
 -include("epgsql.hrl").
 
--type sql_query() :: string() | iodata().
+-type sql_query() :: iodata().
 -type host() :: inet:ip_address() | inet:hostname().
 -type connection() :: pid().
 -type connect_option() ::
@@ -47,75 +52,69 @@
     {database, DBName     :: string()}             |
     {port,     PortNum    :: inet:port_number()}   |
     {ssl,      IsEnabled  :: boolean() | required} |
-    {ssl_opts, SslOptions :: [ssl:ssl_option()]}   | % @see OTP ssl app, ssl_api.hrl
+    {ssl_opts, SslOptions :: [ssl:ssl_option()]}   | % see OTP ssl app, ssl_api.hrl
     {timeout,  TimeoutMs  :: timeout()}            | % default: 5000 ms
     {async,    Receiver   :: pid() | atom()}       | % process to receive LISTEN/NOTIFY msgs
+    {codecs,   Codecs     :: [{epgsql_codec:codec_mod(), any()}]} |
     {replication, Replication :: string()}. % Pass "database" to connect in replication mode
 
--ifdef(have_maps).
 -type connect_opts() ::
         [connect_option()]
       | #{host => host(),
           username => string(),
-          password => string(),
+          password => iodata() | fun( () -> iodata() ),
           database => string(),
           port => inet:port_number(),
           ssl => boolean() | required,
           ssl_opts => [ssl:ssl_option()],
           timeout => timeout(),
-          async => pid(),
+          async => pid() | atom(),
+          codecs => [{epgsql_codec:codec_mod(), any()}],
           replication => string()}.
--else.
--type connect_opts() :: [connect_option()].
--endif.
 
--type connect_error() ::
-        #error{}
-      | {unsupported_auth_method, atom()}
-      | invalid_authorization_specification
-      | invalid_password.
+-type connect_error() :: epgsql_cmd_connect:connect_error().
 -type query_error() :: #error{}.
 
-%% Ranges are from https://www.postgresql.org/docs/current/static/datatype-datetime.html
--type pg_date() ::
-        {Year :: -4712..294276,
-         Month :: 1..12,
-         Day :: 1..31}.
--type pg_time() ::
-        {Hour :: 0..24,  % Max value is 24:00:00
-         Minute :: 0..59,
-         Second :: 0..59 | float()}.
--type pg_datetime() :: {pg_date(), pg_time()}.
--type pg_interval() :: {pg_time(), Days :: integer(), Months :: integer()}.
-
--type bind_param() ::
-        null
-        | boolean()
-        | string()
-        | binary()
-        | integer()
-        | float()
-        | pg_date()
-        | pg_time()
-        | pg_datetime()
-        | pg_interval()
-        | {list({binary(), binary() | null})}   % hstore
-        | [bind_param()].                       %array (maybe nested)
-
--type typed_param() ::
-    {epgsql_type(), bind_param()}.
 
+-type type_name() :: atom().
+-type epgsql_type() :: type_name()
+                     | {array, type_name()}
+                     | {unknown_oid, integer()}.
+
+%% Deprecated
+-type pg_date() :: epgsql_codec_datetime:pg_date().
+-type pg_time() :: epgsql_codec_datetime:pg_time().
+-type pg_datetime() :: epgsql_codec_datetime:pg_datetime().
+-type pg_interval() :: epgsql_codec_datetime:pg_interval().
+
+%% Deprecated
+-type bind_param() :: any().
+
+-type typed_param() :: {epgsql_type(), bind_param()}.
+
+-type column() :: #column{}.
+-type statement() :: #statement{}.
 -type squery_row() :: tuple(). % tuple of binary().
 -type equery_row() :: tuple(). % tuple of bind_param().
 -type ok_reply(RowType) ::
-    {ok, ColumnsDescription :: [#column{}], RowsValues :: [RowType]} |                            % select
-    {ok, Count :: non_neg_integer()} |                                                            % update/insert/delete
-    {ok, Count :: non_neg_integer(), ColumnsDescription :: [#column{}], RowsValues :: [RowType]}. % update/insert/delete + returning
+        %% select
+    {ok, ColumnsDescription :: [column()], RowsValues :: [RowType]} |
+        %% update/insert/delete
+    {ok, Count :: non_neg_integer()} |
+        %% update/insert/delete + returning
+    {ok, Count :: non_neg_integer(), ColumnsDescription :: [column()], RowsValues :: [RowType]}.
 -type error_reply() :: {error, query_error()}.
 -type reply(RowType) :: ok_reply(RowType) | error_reply().
 -type lsn() :: integer().
 -type cb_state() :: term().
 
+%% See https://github.com/erlang/rebar3/pull/1773
+-ifndef(OTP_RELEASE).                           % pre-OTP21
+-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(), ).
+-else.
+-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
+-endif.
+
 %% -- behaviour callbacks --
 
 %% Handles a XLogData Message (StartLSN, EndLSN, WALRecord, CbState).
@@ -126,12 +125,9 @@
 %% -- client interface --
 -spec connect(connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(Settings0) ->
-    Settings = to_proplist(Settings0),
-	Host = proplists:get_value(host, Settings, "localhost"),
-	Username = proplists:get_value(username, Settings, os:getenv("USER")),
-	Password = proplists:get_value(password, Settings, ""),
-	connect(Host, Username, Password, Settings).
+connect(Opts) ->
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -146,7 +142,7 @@ connect(Host, Username, Opts) ->
 %% `Host'     - host to connect to
 %% `Username' - username to connect as, defaults to `$USER'
 %% `Password' - optional password to authenticate with
-%% `Opts'     - proplist of extra options
+%% `Opts'     - proplist or map of extra options
 %% returns `{ok, Connection}' otherwise `{error, Reason}'
 connect(Host, Username, Password, Opts) ->
     {ok, C} = epgsql_sock:start_link(),
@@ -154,42 +150,56 @@ connect(Host, Username, Password, Opts) ->
 
 -spec connect(connection(), host(), string(), string(), connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(C, Host, Username, Password, Opts0) ->
-    Opts = to_proplist(Opts0),
-    %% TODO connect timeout
-    case gen_server:call(C,
-                         {connect, Host, Username, Password, Opts},
-                         infinity) of
+connect(C, Host, Username, Password, Opts) ->
+    Opts1 = maps:merge(to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+-spec call_connect(connection(), connect_opts())
+       -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(to_map(Opts)),
+    case epgsql_sock:sync_command(
+           C, epgsql_cmd_connect, Opts1) of
         connected ->
-            case proplists:get_value(replication, Opts, undefined) of
-                undefined ->
-                    update_type_cache(C),
-                    {ok, C};
-                _ -> {ok, C} %% do not update update_type_cache if connection is in replication mode
-            end;
+            %% If following call fails for you, try to add {codecs, []} connect option
+            {ok, _} = maybe_update_typecache(C, Opts1),
+            {ok, C};
         Error = {error, _} ->
             Error
     end.
 
--spec update_type_cache(connection()) -> ok.
-update_type_cache(C) ->
-    update_type_cache(C, [<<"hstore">>,<<"geometry">>]).
-
--spec update_type_cache(connection(), [binary()]) -> ok.
-update_type_cache(C, DynamicTypes) ->
-    Query = "SELECT typname, oid::int4, typarray::int4"
-            " FROM pg_type"
-            " WHERE typname = ANY($1::varchar[])",
-    case equery(C, Query, [DynamicTypes]) of
-        {ok, _, TypeInfos} ->
-            ok = gen_server:call(C, {update_type_cache, TypeInfos});
-        {error, {error, error, _, _,
-                 <<"column \"typarray\" does not exist in pg_type">>, _}} ->
-            %% Do not fail connect if pg_type table in not in the expected
-            %% format. Known to happen for Redshift which is based on PG v8.0.2
-            ok
-    end.
 
+maybe_update_typecache(C, Opts) ->
+    maybe_update_typecache(C, maps:get(replication, Opts, undefined), maps:get(codecs, Opts, undefined)).
+
+maybe_update_typecache(C, undefined, undefined) ->
+    %% TODO: don't execute 'update_type_cache' when `codecs` is undefined.
+    %% This will break backward compatibility
+    update_type_cache(C);
+maybe_update_typecache(C, undefined, [_ | _] = Codecs) ->
+    update_type_cache(C, Codecs);
+maybe_update_typecache(_, _, _) ->
+    {ok, []}.
+
+update_type_cache(C) ->
+    update_type_cache(C, [{epgsql_codec_hstore, []},
+                          {epgsql_codec_postgis, []}]).
+
+-spec update_type_cache(connection(), [{epgsql_codec:codec_mod(), Opts :: any()}]) ->
+                               epgsql_cmd_update_type_cache:response() | {error, empty}.
+update_type_cache(_C, []) ->
+    {error, empty};
+update_type_cache(C, Codecs) ->
+    %% {error, #error{severity = error,
+    %%                message = <<"column \"typarray\" does not exist in pg_type">>}}
+    %% Do not fail connect if pg_type table in not in the expected
+    %% format. Known to happen for Redshift which is based on PG v8.0.2
+    epgsql_sock:sync_command(C, epgsql_cmd_update_type_cache, Codecs).
+
+%% @doc close connection
 -spec close(connection()) -> ok.
 close(C) ->
     epgsql_sock:close(C).
@@ -213,10 +223,10 @@ set_notice_receiver(C, PidOrName) ->
 get_cmd_status(C) ->
     epgsql_sock:get_cmd_status(C).
 
--spec squery(connection(), sql_query()) -> reply(squery_row()) | [reply(squery_row())].
+-spec squery(connection(), sql_query()) -> epgsql_cmd_squery:response().
 %% @doc runs simple `SqlQuery' via given `Connection'
 squery(Connection, SqlQuery) ->
-    gen_server:call(Connection, {squery, SqlQuery}, infinity).
+    epgsql_sock:sync_command(Connection, epgsql_cmd_squery, SqlQuery).
 
 equery(C, Sql) ->
     equery(C, Sql, []).
@@ -225,28 +235,30 @@ equery(C, Sql) ->
 equery(C, Sql, Parameters) ->
     case parse(C, "", Sql, []) of
         {ok, #statement{types = Types} = S} ->
-            Typed_Parameters = lists:zip(Types, Parameters),
-            gen_server:call(C, {equery, S, Typed_Parameters}, infinity);
+            TypedParameters = lists:zip(Types, Parameters),
+            epgsql_sock:sync_command(C, epgsql_cmd_equery, {S, TypedParameters});
         Error ->
             Error
     end.
 
--spec equery(connection(), string(), sql_query(), [bind_param()]) -> reply(equery_row()).
+-spec equery(connection(), string(), sql_query(), [bind_param()]) ->
+                    epgsql_cmd_equery:response().
 equery(C, Name, Sql, Parameters) ->
     case parse(C, Name, Sql, []) of
         {ok, #statement{types = Types} = S} ->
-            Typed_Parameters = lists:zip(Types, Parameters),
-            gen_server:call(C, {equery, S, Typed_Parameters}, infinity);
+            TypedParameters = lists:zip(Types, Parameters),
+            epgsql_sock:sync_command(C, epgsql_cmd_equery, {S, TypedParameters});
         Error ->
             Error
     end.
 
--spec prepared_query(C::connection(), Name::string(), Parameters::[bind_param()]) -> reply(equery_row()).
+-spec prepared_query(C::connection(), Name::string(), Parameters::[bind_param()]) ->
+                            epgsql_cmd_prepared_query:response().
 prepared_query(C, Name, Parameters) ->
     case describe(C, statement, Name) of
         {ok, #statement{types = Types} = S} ->
-            Typed_Parameters = lists:zip(Types, Parameters),
-            gen_server:call(C, {prepared_query, S, Typed_Parameters}, infinity);
+            TypedParameters = lists:zip(Types, Parameters),
+            epgsql_sock:sync_command(C, epgsql_cmd_prepared_query, {S, TypedParameters});
         Error ->
             Error
     end.
@@ -261,21 +273,24 @@ parse(C, Sql, Types) ->
     parse(C, "", Sql, Types).
 
 -spec parse(connection(), iolist(), sql_query(), [epgsql_type()]) ->
-                   {ok, #statement{}} | {error, query_error()}.
+                   epgsql_cmd_parse:response().
 parse(C, Name, Sql, Types) ->
-    sync_on_error(C, gen_server:call(C, {parse, Name, Sql, Types}, infinity)).
+    sync_on_error(
+      C, epgsql_sock:sync_command(
+           C, epgsql_cmd_parse, {Name, Sql, Types})).
 
 %% bind
 
 bind(C, Statement, Parameters) ->
     bind(C, Statement, "", Parameters).
 
--spec bind(connection(), #statement{}, string(), [bind_param()]) ->
-                  ok | {error, query_error()}.
+-spec bind(connection(), statement(), string(), [bind_param()]) ->
+                  epgsql_cmd_bind:response().
 bind(C, Statement, PortalName, Parameters) ->
     sync_on_error(
       C,
-      gen_server:call(C, {bind, Statement, PortalName, Parameters}, infinity)).
+      epgsql_sock:sync_command(
+        C, epgsql_cmd_bind, {Statement, PortalName, Parameters})).
 
 %% execute
 
@@ -285,39 +300,45 @@ execute(C, S) ->
 execute(C, S, N) ->
     execute(C, S, "", N).
 
--spec execute(connection(), #statement{}, string(), non_neg_integer()) -> Reply
-                                                                              when
-      Reply :: {ok | partial, [equery_row()]}
-             | {ok, non_neg_integer()}
-             | {ok, non_neg_integer(), [equery_row()]}
-             | {error, query_error()}.
+-spec execute(connection(), statement(), string(), non_neg_integer()) -> Reply when
+      Reply :: epgsql_cmd_execute:response().
 execute(C, S, PortalName, N) ->
-    gen_server:call(C, {execute, S, PortalName, N}, infinity).
+    epgsql_sock:sync_command(C, epgsql_cmd_execute, {S, PortalName, N}).
 
--spec execute_batch(connection(), [{#statement{}, [bind_param()]}]) -> [reply(equery_row())].
+-spec execute_batch(connection(), [{statement(), [bind_param()]}]) ->
+                           epgsql_cmd_batch:response().
 execute_batch(C, Batch) ->
-    gen_server:call(C, {execute_batch, Batch}, infinity).
+    epgsql_sock:sync_command(C, epgsql_cmd_batch, Batch).
 
 %% statement/portal functions
-
+-spec describe(connection(), statement()) -> epgsql_cmd_describe_statement:response().
 describe(C, #statement{name = Name}) ->
     describe(C, statement, Name).
 
+-spec describe(connection(), portal, iodata()) -> epgsql_cmd_describe_portal:response();
+              (connection(), statement, iodata()) -> epgsql_cmd_describe_statement:response().
 describe(C, statement, Name) ->
-    sync_on_error(C, gen_server:call(C, {describe_statement, Name}, infinity));
+    sync_on_error(
+      C, epgsql_sock:sync_command(
+           C, epgsql_cmd_describe_statement, Name));
 
-%% TODO unknown result format of Describe portal
 describe(C, portal, Name) ->
-    sync_on_error(C, gen_server:call(C, {describe_portal, Name}, infinity)).
+    sync_on_error(
+      C, epgsql_sock:sync_command(
+           C, epgsql_cmd_describe_portal, Name)).
 
+%% @doc close statement
+-spec close(connection(), statement()) -> epgsql_cmd_close:response().
 close(C, #statement{name = Name}) ->
     close(C, statement, Name).
 
+-spec close(connection(), statement | portal, iodata()) -> epgsql_cmd_close:response().
 close(C, Type, Name) ->
-    gen_server:call(C, {close, Type, Name}).
+    epgsql_sock:sync_command(C, epgsql_cmd_close, {Type, Name}).
 
+-spec sync(connection()) -> epgsql_cmd_sync:response().
 sync(C) ->
-    gen_server:call(C, sync).
+    epgsql_sock:sync_command(C, epgsql_cmd_sync, []).
 
 -spec cancel(connection()) -> ok.
 cancel(C) ->
@@ -329,15 +350,53 @@ cancel(C) ->
                                   when
       Reply :: any().
 with_transaction(C, F) ->
-    try {ok, [], []} = squery(C, "BEGIN"),
+    with_transaction(C, F, [{reraise, false}]).
+
+%% @doc Execute callback function with connection in a transaction.
+%% Transaction will be rolled back in case of exception.
+%% 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,
+%%   check that transaction was comitted by checking CommandComplete status
+%%   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
+%%   https://www.postgresql.org/docs/current/static/sql-begin.html)
+%%   Beware of SQL injections! No escaping is made on begin_opts!
+-spec with_transaction(
+        connection(), fun((connection()) -> Reply), Opts) -> Reply | {rollback, any()} | no_return() when
+      Reply :: any(),
+      Opts :: [{reraise, boolean()} |
+               {ensure_committed, boolean()} |
+               {begin_opts, iodata()}].
+with_transaction(C, F, Opts0) ->
+    Opts = to_map(Opts0),
+    Begin = case Opts of
+                #{begin_opts := BeginOpts} ->
+                    [<<"BEGIN ">> | BeginOpts];
+                _ -> <<"BEGIN">>
+            end,
+    try
+        {ok, [], []} = squery(C, Begin),
         R = F(C),
-        {ok, [], []} = squery(C, "COMMIT"),
+        {ok, [], []} = squery(C, <<"COMMIT">>),
+        case Opts of
+            #{ensure_committed := true} ->
+                {ok, CmdStatus} = get_cmd_status(C),
+                (commit == CmdStatus) orelse error({ensure_committed_failed, CmdStatus});
+            _ -> ok
+        end,
         R
     catch
-        _:Why ->
+        ?WITH_STACKTRACE(Type, Reason, Stack)
             squery(C, "ROLLBACK"),
-            %% TODO hides error stacktrace
-            {rollback, Why}
+            case maps:get(reraise, Opts, true) of
+                true ->
+                    erlang:raise(Type, Reason, Stack);
+                false ->
+                    {rollback, Reason}
+            end
     end.
 
 sync_on_error(C, Error = {error, _}) ->
@@ -347,25 +406,25 @@ sync_on_error(C, Error = {error, _}) ->
 sync_on_error(_C, R) ->
     R.
 
--spec standby_status_update(connection(), lsn(), lsn()) -> ok | error_reply().
-%% @doc sends last flushed and applied WAL positions to the server in a standby status update message via given `Connection'
+-spec standby_status_update(connection(), lsn(), lsn()) -> ok.
+%% @doc sends last flushed and applied WAL positions to the server in a standby status update message via
+%% given `Connection'
 standby_status_update(Connection, FlushedLSN, AppliedLSN) ->
     gen_server:call(Connection, {standby_status_update, FlushedLSN, AppliedLSN}).
 
+handle_x_log_data(Mod, StartLSN, EndLSN, WALRecord, Repl) ->
+    Mod:handle_x_log_data(StartLSN, EndLSN, WALRecord, Repl).
+
 -type replication_option() ::
     {align_lsn, boolean()}. %% Align last applied and flushed LSN with last received LSN
                             %%  after Primary keepalive message with ReplyRequired flag
 
--ifdef(have_maps).
 -type replication_opts() ::
     [replication_option()]
     | #{align_lsn => boolean()}.
--else.
-    -type replication_opts() :: [replication_option()].
--endif.
 
--spec start_replication(connection(), string(), Callback, cb_state(), string(), string(), replication_opts()) ->
-    ok | error_reply() when
+-spec start_replication(connection(), string(), Callback, cb_state(), string(), string(), replication_opts()) -> Response when
+    Response :: epgsql_cmd_start_replication:response(),
     Callback :: module() | pid().
 %% @doc instructs Postgres server to start streaming WAL for logical replication
 %% where
@@ -380,17 +439,17 @@ standby_status_update(Connection, FlushedLSN, AppliedLSN) ->
 %%                      For example: "option_name1 'value1', option_name2 'value2'"
 %% `Opts'            - options of logical replication
 %% returns `ok' otherwise `{error, Reason}'
-start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts0) ->
-    Opts = to_proplist(Opts0),
-    gen_server:call(Connection,
-        {start_replication, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts}).
+start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts) ->
+    Command = {ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, to_map(Opts)},
+    epgsql_sock:sync_command(Connection, epgsql_cmd_start_replication, Command).
 start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts) ->
     start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, []).
 start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition) ->
     start_replication(Connection, ReplicationSlot, Callback, CbInitState, WALPosition, [], []).
 
 %% @private
-to_proplist(List) when is_list(List) ->
-    List;
-to_proplist(Map) ->
-    maps:to_list(Map).
+-spec to_map([{any(), any()}] | map()) -> map().
+to_map(Map) when is_map(Map) ->
+    Map;
+to_map(List) when is_list(List) ->
+    maps:from_list(List).

+ 299 - 320
src/epgsql_binary.erl

@@ -1,337 +1,316 @@
 %%% Copyright (C) 2008 - Will Glozer.  All rights reserved.
-
+%% XXX: maybe merge this module into epgsql_codec?
 -module(epgsql_binary).
 
--export([new_codec/1,
-         update_type_cache/2,
-         type2oid/2, oid2type/2,
-         encode/3, decode/3, supports/1]).
-
--record(codec, {
-    type2oid = [],
-    oid2type = []
-}).
-
--include("epgsql_binary.hrl").
-
--define(datetime, (get(datetime_mod))).
-
--define(INET, 2).
--define(INET6, 3).
--define(IP_SIZE, 4).
--define(IP6_SIZE, 16).
--define(MAX_IP_MASK, 32).
--define(MAX_IP6_MASK, 128).
--define(JSONB_VERSION_1, 1).
-
-new_codec([]) -> #codec{}.
-
-update_type_cache(TypeInfos, Codec) ->
-    Type2Oid = lists:flatmap(
-        fun({NameBin, ElementOid, ArrayOid}) ->
-            Name = erlang:binary_to_atom(NameBin, utf8),
-            [{Name, ElementOid}, {{array, Name}, ArrayOid}]
-        end,
-        TypeInfos),
-    Oid2Type = [{Oid, Type} || {Type, Oid} <- Type2Oid],
-    Codec#codec{type2oid = Type2Oid, oid2type = Oid2Type}.
-
-oid2type(Oid, #codec{oid2type = Oid2Type}) ->
-    case epgsql_types:oid2type(Oid) of
-        {unknown_oid, _} ->
-            proplists:get_value(Oid, Oid2Type, {unknown_oid, Oid});
-        Type -> Type
+-export([new_codec/2,
+         update_codec/2,
+         type_to_oid/2,
+         typeinfo_to_name_array/2,
+         typeinfo_to_oid_info/2,
+         oid_to_name/2,
+         oid_to_info/2,
+         oid_to_decoder/3,
+         decode/2, encode/3, supports/2]).
+%% Composite type decoders
+-export([decode_record/3, decode_array/3]).
+
+-export_type([codec/0, decoder/0]).
+
+-include("protocol.hrl").
+
+-record(codec,
+        {opts = [] :: list(),                   % not used yet
+         oid_db :: epgsql_oid_db:db()}).
+
+-opaque codec() :: #codec{}.
+-opaque decoder() :: {fun((binary(), epgsql:type_name(), epgsql_codec:codec_state()) -> any()),
+                      epgsql:type_name(),
+                      epgsql_codec:codec_state()}.
+
+-type type() :: epgsql:type_name() | {array, epgsql:type_name()}.
+-type maybe_unknown_type() :: type() | {unknown_oid, epgsql_oid_db:oid()}.
+
+-define(RECORD_OID, 2249).
+-define(RECORD_ARRAY_OID, 2287).
+
+%% 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)
+
+-spec new_codec(epgsql_sock:pg_sock(), list()) -> codec().
+new_codec(PgSock, Opts) ->
+    Codecs = default_codecs(),
+    Oids = default_oids(),
+    new_codec(PgSock, Codecs, Oids, Opts).
+
+new_codec(PgSock, Codecs, Oids, Opts) ->
+    CodecEntries = epgsql_codec:init_mods(Codecs, PgSock),
+    Types = epgsql_oid_db:join_codecs_oids(Oids, CodecEntries),
+    #codec{oid_db = epgsql_oid_db:from_list(Types), opts = Opts}.
+
+-spec update_codec([epgsql_oid_db:type_info()], codec()) -> codec().
+update_codec(TypeInfos, #codec{oid_db = Db} = Codec) ->
+    Codec#codec{oid_db = epgsql_oid_db:update(TypeInfos, Db)}.
+
+-spec oid_to_name(epgsql_oid_db:oid(), codec()) -> maybe_unknown_type().
+oid_to_name(Oid, Codec) ->
+    case oid_to_info(Oid, Codec) of
+        undefined ->
+            {unknown_oid, Oid};
+        Type ->
+            case epgsql_oid_db:type_to_oid_info(Type) of
+                {_, Name, true} -> {array, Name};
+                {_, Name, false} -> Name
+            end
+    end.
+
+-spec type_to_oid(type(), codec()) -> epgsql_oid_db:oid().
+type_to_oid({array, Name}, Codec) ->
+    type_to_oid(Name, true, Codec);
+type_to_oid(Name, Codec) ->
+    type_to_oid(Name, false, Codec).
+
+-spec type_to_oid(epgsql:type_name(), boolean(), codec()) -> epgsql_oid_db:oid().
+type_to_oid(TypeName, IsArray, #codec{oid_db = Db}) ->
+    epgsql_oid_db:oid_by_name(TypeName, IsArray, Db).
+
+-spec type_to_type_info(type(), codec()) -> epgsql_oid_db:type_info() | undefined.
+type_to_type_info({array, Name}, Codec) ->
+    type_to_info(Name, true, Codec);
+type_to_type_info(Name, Codec) ->
+    type_to_info(Name, false, Codec).
+
+-spec oid_to_info(epgsql_oid_db:oid(), codec()) -> epgsql_oid_db:type_info() | undefined.
+oid_to_info(Oid, #codec{oid_db = Db}) ->
+    epgsql_oid_db:find_by_oid(Oid, Db).
+
+-spec type_to_info(epgsql:type_name(), boolean(), codec()) -> epgsql_oid_db:type_info().
+type_to_info(TypeName, IsArray, #codec{oid_db = Db}) ->
+    epgsql_oid_db:find_by_name(TypeName, IsArray, Db).
+
+-spec typeinfo_to_name_array(Unknown | epgsql_oid_db:type_info(), _) -> Unknown | type() when
+      Unknown :: {unknown_oid, epgsql_oid_db:oid()}.
+typeinfo_to_name_array({unknown_oid, _} = Unknown, _) -> Unknown;
+typeinfo_to_name_array(TypeInfo, _) ->
+    case epgsql_oid_db:type_to_oid_info(TypeInfo) of
+        {_, Name, false} -> Name;
+        {_, Name, true} -> {array, Name}
     end.
 
-type2oid(Type, #codec{type2oid = Type2Oid}) ->
-    case epgsql_types:type2oid(Type) of
-        {unknown_type, _} ->
-            proplists:get_value(Type, Type2Oid, {unknown_type, Type});
-        Oid -> Oid
+-spec typeinfo_to_oid_info(Unknown | epgsql_oid_db:type_info(), _) ->
+                                  Unknown | epgsql_oid_db:oid_info() when
+      Unknown :: {unknown_oid, epgsql_oid_db:oid()}.
+typeinfo_to_oid_info({unknown_oid, _} = Unknown, _) -> Unknown;
+typeinfo_to_oid_info(TypeInfo, _) ->
+    epgsql_oid_db:type_to_oid_info(TypeInfo).
+
+%%
+%% Decode
+%%
+
+%% @doc decode single cell
+-spec decode(binary(), decoder()) -> any().
+decode(Bin, {Fun, TypeName, State}) ->
+    Fun(Bin, TypeName, State).
+
+%% @doc generate decoder to decode PG binary of datatype specified as OID
+-spec oid_to_decoder(epgsql_oid_db:oid(), binary | text, codec()) -> decoder().
+oid_to_decoder(?RECORD_OID, binary, Codec) ->
+    {fun ?MODULE:decode_record/3, record, 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}) ->
+    case epgsql_oid_db:find_by_oid(Oid, Db) of
+        undefined when Format == binary ->
+            {fun epgsql_codec_noop:decode/3, undefined, []};
+        undefined when Format == text ->
+            {fun epgsql_codec_noop:decode_text/3, undefined, []};
+        Type ->
+            make_decoder(Type, Format)
+    end.
+
+-spec make_decoder(epgsql_oid_db:type_info(), binary | text) -> decoder().
+make_decoder(Type, Format) ->
+    {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
+    {_Oid, Name, IsArray} = epgsql_oid_db:type_to_oid_info(Type),
+    make_decoder(Name, Mod, State, Format, IsArray).
+
+make_decoder(_Name, _Mod, _State, text, true) ->
+    %% Don't try to decode text arrays
+    {fun epgsql_codec_noop:decode_text/3, undefined, []};
+make_decoder(Name, Mod, State, text, false) ->
+    %% decode_text/3 is optional callback. If it's not defined, do NOOP.
+    case erlang:function_exported(Mod, decode_text, 3) of
+        true ->
+            {fun Mod:decode_text/3, Name, State};
+        false ->
+            {fun epgsql_codec_noop:decode_text/3, undefined, []}
+    end;
+make_decoder(Name, Mod, State, binary, true) ->
+    make_array_decoder(Name, Mod, State);
+make_decoder(Name, Mod, State, binary, false) ->
+    {fun Mod:decode/3, Name, State}.
+
+
+%% Array decoding
+%%% $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) ->
+    %% 4b: n_dimensions;
+    %% 4b: flags;
+    %% 4b: Oid // should be the same as in column spec;
+    %%   (4b: n_elements;
+    %%    4b: lower_bound) * n_dimensions
+    %% (dynamic-size data)
+    %% Lower bound - eg, zero-bound or 1-bound or N-bound array. We ignore it, see
+    %% https://www.postgresql.org/docs/current/static/arrays.html#arrays-io
+    {Dims, Data} = erlang:split_binary(Rest, NDims * 2 * 4),
+    Lengths = [Len || <<Len:?int32, _LBound:?int32>> <= Dims],
+    {Array, <<>>} = decode_array1(Data, Lengths, ElemDecoder),
+    Array.
+
+decode_array1(Data, [], _)  ->
+    %% zero-dimensional array
+    {[], Data};
+decode_array1(Data, [Len], ElemDecoder) ->
+    %% 1-dimensional array
+    decode_elements(Data, [], Len, ElemDecoder);
+decode_array1(Data, [Len | T], ElemDecoder) ->
+    %% multidimensional array
+    F = fun(_N, Rest) -> decode_array1(Rest, T, ElemDecoder) end,
+    lists:mapfoldl(F, Data, lists:seq(1, Len)).
+
+decode_elements(Rest, Acc, 0, _ElDec) ->
+    {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) ->
+    Value2 = decode(Value, ElemDecoder),
+    decode_elements(Rest, [Value2 | Acc], N - 1, ElemDecoder).
+
+
+
+%% Record decoding
+%% $PG$/src/backend/utils/adt/rowtypes.c
+decode_record(<<Size:?int32, Bin/binary>>, record, Codec) ->
+    list_to_tuple(decode_record1(Bin, Size, Codec)).
+
+decode_record1(<<>>, 0, _Codec) -> [];
+decode_record1(<<_Type:?int32, -1:?int32, Rest/binary>>, Size, Codec) ->
+    [null | decode_record1(Rest, Size - 1, 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_record1(Rest, Size - 1, Codec)].
+
+
+%%
+%% Encode
+%%
+
+%% Convert erlang value to PG binary of type, specified by type name
+-spec encode(epgsql:type_name() | {array, epgsql:type_name()}, any(), codec()) -> iolist().
+encode(TypeName, Value, Codec) ->
+    Type = type_to_type_info(TypeName, Codec),
+    encode_with_type(Type, Value).
+
+encode_with_type(Type, Value) ->
+    {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
+    case epgsql_oid_db:type_to_oid_info(Type) of
+        {_ArrayOid, _, true} ->
+            %FIXME: check if this OID is the same as was returned by 'Describe'
+            ElementOid = epgsql_oid_db:type_to_element_oid(Type),
+            encode_array(Value, ElementOid, {Mod, Name, State});
+        {_Oid, _, false} ->
+            encode_value(Value, {Mod, Name, State})
     end.
 
-encode(_Any, null, _)                       -> <<-1:?int32>>;
-encode(_Any, undefined, _)                  -> <<-1:?int32>>;
-encode(bool, true, _)                       -> <<1:?int32, 1:1/big-signed-unit:8>>;
-encode(bool, false, _)                      -> <<1:?int32, 0:1/big-signed-unit:8>>;
-encode(int2, N, _)                          -> <<2:?int32, N:1/big-signed-unit:16>>;
-encode(int4, N, _)                          -> <<4:?int32, N:1/big-signed-unit:32>>;
-encode(int8, N, _)                          -> <<8:?int32, N:1/big-signed-unit:64>>;
-encode(float4, N, _)                        -> <<4:?int32, N:1/big-float-unit:32>>;
-encode(float8, N, _)                        -> <<8:?int32, N:1/big-float-unit:64>>;
-encode(bpchar, C, _) when is_integer(C)     -> <<1:?int32, C:1/big-unsigned-unit:8>>;
-encode(bpchar, B, _) when is_binary(B)      -> <<(byte_size(B)):?int32, B/binary>>;
-encode(time = Type, B, _)                   -> ?datetime:encode(Type, B);
-encode(timetz = Type, B, _)                 -> ?datetime:encode(Type, B);
-encode(date = Type, B, _)                   -> ?datetime:encode(Type, B);
-encode(timestamp = Type, B, _)              -> ?datetime:encode(Type, B);
-encode(timestamptz = Type, B, _)            -> ?datetime:encode(Type, B);
-encode(interval = Type, B, _)               -> ?datetime:encode(Type, B);
-encode(bytea, B, _) when is_binary(B)       -> <<(byte_size(B)):?int32, B/binary>>;
-encode(text, B, _) when is_binary(B)        -> <<(byte_size(B)):?int32, B/binary>>;
-encode(varchar, B, _) when is_binary(B)     -> <<(byte_size(B)):?int32, B/binary>>;
-encode(json, B, _) when is_binary(B)        -> <<(byte_size(B)):?int32, B/binary>>;
-encode(jsonb, B, _) when is_binary(B)       -> <<(byte_size(B) + 1):?int32, ?JSONB_VERSION_1:8, B/binary>>;
-encode(uuid, B, _) when is_binary(B)        -> encode_uuid(B);
-encode({array, char}, L, Codec) when is_list(L) -> encode_array(bpchar, type2oid(bpchar, Codec), L, Codec);
-encode({array, Type}, L, Codec) when is_list(L) -> encode_array(Type, type2oid(Type, Codec), L, Codec);
-encode(hstore, {L}, _) when is_list(L)      -> encode_hstore(L);
-encode(point, {X,Y}, _)                     -> encode_point({X,Y});
-encode(geometry, Data, _)                   -> encode_geometry(Data);
-encode(cidr, B, Codec)                      -> encode(bytea, encode_net(B), Codec);
-encode(inet, B, Codec)                      -> encode(bytea, encode_net(B), Codec);
-encode(int4range, R, _) when is_tuple(R)    -> encode_int4range(R);
-encode(int8range, R, _) when is_tuple(R)    -> encode_int8range(R);
-encode(Type, L, Codec) when is_list(L)      -> encode(Type, list_to_binary(L), Codec);
-encode(_Type, _Value, _)                    -> {error, unsupported}.
-
-decode(bool, <<1:1/big-signed-unit:8>>, _)     -> true;
-decode(bool, <<0:1/big-signed-unit:8>>, _)     -> false;
-decode(bpchar, <<C:1/big-unsigned-unit:8>>, _) -> C;
-decode(int2, <<N:1/big-signed-unit:16>>, _)    -> N;
-decode(int4, <<N:1/big-signed-unit:32>>, _)    -> N;
-decode(int8, <<N:1/big-signed-unit:64>>, _)    -> N;
-decode(float4, <<N:1/big-float-unit:32>>, _)   -> N;
-decode(float8, <<N:1/big-float-unit:64>>, _)   -> N;
-decode(record, <<_:?int32, Rest/binary>>, Codec) -> list_to_tuple(decode_record(Rest, [], Codec));
-decode(jsonb, <<?JSONB_VERSION_1:8, Value/binary>>, _) -> Value;
-decode(time = Type, B, _)                      -> ?datetime:decode(Type, B);
-decode(timetz = Type, B, _)                    -> ?datetime:decode(Type, B);
-decode(date = Type, B, _)                      -> ?datetime:decode(Type, B);
-decode(timestamp = Type, B, _)                 -> ?datetime:decode(Type, B);
-decode(timestamptz = Type, B, _)               -> ?datetime:decode(Type, B);
-decode(interval = Type, B, _)                  -> ?datetime:decode(Type, B);
-decode(uuid, B, _)                             -> decode_uuid(B);
-decode(hstore, Hstore, _)                      -> decode_hstore(Hstore);
-decode(inet, B, _)                             -> decode_net(B);
-decode(cidr, B, _)                             -> decode_net(B);
-decode({array, _Type}, B, Codec)               -> decode_array(B, Codec);
-decode(point, B, _)                            -> decode_point(B);
-decode(geometry, B, _)                         -> ewkb:decode_geometry(B);
-decode(int4range, B, _)                        -> decode_int4range(B);
-decode(int8range, B, _)                        -> decode_int8range(B);
-decode(_Other, Bin, _)                         -> Bin.
-
-encode_array(Type, Oid, A, Codec) ->
-    {Data, {NDims, Lengths}} = encode_array(Type, A, 0, [], Codec),
+encode_value(Value, {Mod, Name, State}) ->
+    Payload = epgsql_codec:encode(Mod, Value, Name, State),
+    [<<(iolist_size(Payload)):?int32>> | Payload].
+
+
+%% Number of dimensions determined at encode-time by introspection of data, so,
+%% we can't encode array of lists (eg. strings).
+encode_array(Array, Oid, ValueEncoder) ->
+    {Data, {NDims, Lengths}} = encode_array(Array, 0, [], ValueEncoder),
     Lens = [<<N:?int32, 1:?int32>> || N <- lists:reverse(Lengths)],
     Hdr  = <<NDims:?int32, 0:?int32, Oid:?int32>>,
-    Bin  = iolist_to_binary([Hdr, Lens, Data]),
-    <<(byte_size(Bin)):?int32, Bin/binary>>.
+    Payload  = [Hdr, Lens, Data],
+    [<<(iolist_size(Payload)):?int32>> | Payload].
 
-encode_array(_Type, [], NDims, Lengths, _Codec) ->
-    {<<>>, {NDims, Lengths}};
-encode_array(Type, [H | _] = Array, NDims, Lengths, Codec) when not is_list(H) ->
-    F = fun(E, Len) -> {encode(Type, E, Codec), Len + 1} end,
+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(uuid, [_H | _] = Array, NDims, Lengths, Codec) ->
-    F = fun(E, Len) -> {encode(uuid, E, Codec), Len + 1} end,
-    {Data, Len} = lists:mapfoldl(F, 0, Array),
-    {Data, {NDims + 1, [Len | Lengths]}};
-encode_array(Type, Array, NDims, Lengths, Codec) ->
+encode_array(Array, NDims, Lengths, Codec) ->
     Lengths2 = [length(Array) | Lengths],
-    F = fun(A2, {_NDims, _Lengths}) -> encode_array(Type, A2, NDims, Lengths2, Codec) end,
+    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_uuid(U) when is_binary(U) ->
-    encode_uuid(binary_to_list(U));
-encode_uuid(U) ->
-    Hex = [H || H <- U, H =/= $-],
-    {ok, [Int], _} = io_lib:fread("~16u", Hex),
-    <<16:?int32,Int:128>>.
-
-encode_hstore(HstoreEntries) ->
-    Body = << <<(encode_hstore_entry(Entry))/binary>> || Entry <- HstoreEntries >>,
-    <<(byte_size(Body) + 4):?int32, (length(HstoreEntries)):?int32, Body/binary>>.
-
-encode_hstore_entry({Key, Value}) ->
-    <<(encode_hstore_key(Key))/binary, (encode_hstore_value(Value))/binary>>.
-
-encode_hstore_key(Key) -> encode_hstore_string(Key).
-
-encode_hstore_value(null)      -> <<-1:?int32>>;
-encode_hstore_value(undefined) -> <<-1:?int32>>;
-encode_hstore_value(Val)       -> encode_hstore_string(Val).
-
-encode_hstore_string(Str) when is_list(Str) -> encode_hstore_string(list_to_binary(Str));
-encode_hstore_string(Str) when is_atom(Str) -> encode_hstore_string(atom_to_binary(Str, utf8));
-encode_hstore_string(Str) when is_integer(Str) ->
-    %% FIXME - we can use integer_to_binary when we deprecate R15
-    encode_hstore_string(list_to_binary(integer_to_list(Str)));
-encode_hstore_string(Str) when is_float(Str) ->
-    encode_hstore_string(iolist_to_binary(io_lib:format("~w", [Str])));
-encode_hstore_string(Str) when is_binary(Str) -> <<(byte_size(Str)):?int32, Str/binary>>.
-
-encode_net({{_, _, _, _} = IP, Mask}) ->
-    Bin = list_to_binary(tuple_to_list(IP)),
-    <<?INET, Mask, 1, ?IP_SIZE, Bin/binary>>;
-encode_net({{_, _, _, _, _, _, _, _} = IP, Mask}) ->
-    Bin = << <<X:16>> || X <- tuple_to_list(IP) >>,
-    <<?INET6, Mask, 1, ?IP6_SIZE, Bin/binary>>;
-encode_net({_, _, _, _} = IP) ->
-    Bin = list_to_binary(tuple_to_list(IP)),
-    <<?INET, ?MAX_IP_MASK, 0, ?IP_SIZE, Bin/binary>>;
-encode_net({_, _, _, _, _, _, _, _} = IP) ->
-    Bin = << <<X:16>> || X <- tuple_to_list(IP) >>,
-    <<?INET6, ?MAX_IP6_MASK, 0, ?IP6_SIZE, Bin/binary>>.
-
-decode_array(<<NDims:?int32, _HasNull:?int32, Oid:?int32, Rest/binary>>, Codec) ->
-    {Dims, Data} = erlang:split_binary(Rest, NDims * 2 * 4),
-    Lengths = [Len || <<Len:?int32, _LBound:?int32>> <= Dims],
-    Type = oid2type(Oid, Codec),
-    {Array, <<>>} = decode_array(Data, Type, Lengths, Codec),
-    Array.
-
-decode_array(Data, _Type, [], _Codec)  ->
-    {[], Data};
-decode_array(Data, Type, [Len], Codec) ->
-    decode_elements(Data, Type, [], Len, Codec);
-decode_array(Data, Type, [Len | T], Codec) ->
-    F = fun(_N, Rest) -> decode_array(Rest, Type, T, Codec) end,
-    lists:mapfoldl(F, Data, lists:seq(1, Len)).
 
-decode_elements(Rest, _Type, Acc, 0, _Codec) ->
-    {lists:reverse(Acc), Rest};
-decode_elements(<<-1:?int32, Rest/binary>>, Type, Acc, N, Codec) ->
-    decode_elements(Rest, Type, [null | Acc], N - 1, Codec);
-decode_elements(<<Len:?int32, Value:Len/binary, Rest/binary>>, Type, Acc, N, Codec) ->
-    Value2 = decode(Type, Value, Codec),
-    decode_elements(Rest, Type, [Value2 | Acc], N - 1, Codec).
-
-decode_record(<<>>, Acc, _Codec) ->
-    lists:reverse(Acc);
-decode_record(<<_Type:?int32, -1:?int32, Rest/binary>>, Acc, Codec) ->
-    decode_record(Rest, [null | Acc], Codec);
-decode_record(<<Type:?int32, Len:?int32, Value:Len/binary, Rest/binary>>, Acc, Codec) ->
-    Value2 = decode(oid2type(Type, Codec), Value, Codec),
-    decode_record(Rest, [Value2 | Acc], Codec).
-
-decode_uuid(<<U0:32, U1:16, U2:16, U3:16, U4:48>>) ->
-    Format = "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",
-    iolist_to_binary(io_lib:format(Format, [U0, U1, U2, U3, U4])).
-
-decode_hstore(<<NumElements:?int32, Elements/binary>>) ->
-    {decode_hstore1(NumElements, Elements, [])}.
-
-decode_hstore1(0, _Elements, Acc) -> Acc;
-decode_hstore1(N, <<KeyLen:?int32, Key:KeyLen/binary, -1:?int32, Rest/binary>>, Acc) ->
-    decode_hstore1(N - 1, Rest, [{Key, null} | Acc]);
-decode_hstore1(N, <<KeyLen:?int32, Key:KeyLen/binary, ValLen:?int32, Value:ValLen/binary, Rest/binary>>, Acc) ->
-    decode_hstore1(N - 1, Rest, [{Key, Value} | Acc]).
-
-encode_point({X, Y}) when is_number(X), is_number(Y) ->
-    <<X:1/big-float-unit:64, Y:1/big-float-unit:64>>.
-
-decode_point(<<X:1/big-float-unit:64, Y:1/big-float-unit:64>>) ->
-    {X, Y}.
-
-encode_geometry(Data) ->
-    Bin = ewkb:encode_geometry(Data),
-    Size = byte_size(Bin),
-    <<Size:?int32, Bin/binary>>.
-
-decode_net(<<?INET, Mask, 1, ?IP_SIZE, Bin/binary>>) ->
-    {list_to_tuple(binary_to_list(Bin)), Mask};
-decode_net(<<?INET6, Mask, 1, ?IP6_SIZE, Bin/binary>>) ->
-    {list_to_tuple([X || <<X:16>> <= Bin]), Mask};
-decode_net(<<?INET, ?MAX_IP_MASK, 0, ?IP_SIZE, Bin/binary>>) ->
-    list_to_tuple(binary_to_list(Bin));
-decode_net(<<?INET6, ?MAX_IP6_MASK, 0, ?IP6_SIZE, Bin/binary>>) ->
-    list_to_tuple([X || <<X:16>> <= Bin]).
-
-%% @doc encode an int4range
-encode_int4range({minus_infinity, plus_infinity}) ->
-    <<1:?int32, 24:1/big-signed-unit:8>>;
-encode_int4range({From, plus_infinity}) ->
-    FromInt = to_int(From),
-    <<9:?int32, 18:1/big-signed-unit:8, 4:?int32, FromInt:?int32>>;
-encode_int4range({minus_infinity, To}) ->
-    ToInt = to_int(To),
-    <<9:?int32, 8:1/big-signed-unit:8, 4:?int32, ToInt:?int32>>;
-encode_int4range({From, To}) ->
-    FromInt = to_int(From),
-    ToInt = to_int(To),
-    <<17:?int32, 2:1/big-signed-unit:8, 4:?int32, FromInt:?int32, 4:?int32, ToInt:?int32>>.
-
-%% @doc encode an int8range
-encode_int8range({minus_infinity, plus_infinity}) ->
-    <<1:?int32, 24:1/big-signed-unit:8>>;
-encode_int8range({From, plus_infinity}) ->
-    FromInt = to_int(From),
-    <<13:?int32, 18:1/big-signed-unit:8, 8:?int32, FromInt:?int64>>;
-encode_int8range({minus_infinity, To}) ->
-    ToInt = to_int(To),
-    <<13:?int32, 8:1/big-signed-unit:8, 8:?int32, ToInt:?int64>>;
-encode_int8range({From, To}) ->
-    FromInt = to_int(From),
-    ToInt = to_int(To),
-    <<25:?int32, 2:1/big-signed-unit:8, 8:?int32, FromInt:?int64, 8:?int32, ToInt:?int64>>.
-
-to_int(N) when is_integer(N) -> N;
-to_int(S) when is_list(S) -> erlang:list_to_integer(S);
-to_int(B) when is_binary(B) -> erlang:binary_to_integer(B).
-
-%% @doc decode an int4range
-decode_int4range(<<2:1/big-signed-unit:8, 4:?int32, From:?int32, 4:?int32, To:?int32>>) -> {From, To};
-decode_int4range(<<8:1/big-signed-unit:8, 4:?int32, To:?int32>>) -> {minus_infinity, To};
-decode_int4range(<<18:1/big-signed-unit:8, 4:?int32, From:?int32>>) -> {From, plus_infinity};
-decode_int4range(<<24:1/big-signed-unit:8>>) -> {minus_infinity, plus_infinity}.
-
-%% @doc decode an int8range
-decode_int8range(<<2:1/big-signed-unit:8, 8:?int32, From:?int64, 8:?int32, To:?int64>>) -> {From, To};
-decode_int8range(<<8:1/big-signed-unit:8, 8:?int32, To:?int64>>) -> {minus_infinity, To};
-decode_int8range(<<18:1/big-signed-unit:8, 8:?int32, From:?int64>>) -> {From, plus_infinity};
-decode_int8range(<<24:1/big-signed-unit:8>>) -> {minus_infinity, plus_infinity}.
-
-supports(bool)    -> true;
-supports(bpchar)  -> true;
-supports(int2)    -> true;
-supports(int4)    -> true;
-supports(int8)    -> true;
-supports(float4)  -> true;
-supports(float8)  -> true;
-supports(bytea)   -> true;
-supports(text)    -> true;
-supports(varchar) -> true;
-supports(record)  -> true;
-supports(date)    -> true;
-supports(time)    -> true;
-supports(timetz)  -> true;
-supports(timestamp)   -> true;
-supports(timestamptz) -> true;
-supports(interval)    -> true;
-supports(uuid)        -> true;
-supports(hstore)      -> true;
-supports(cidr)        -> true;
-supports(inet)        -> true;
-supports(geometry)    -> true;
-supports(point)       -> true;
-supports(json)        -> true;
-supports(jsonb)        -> true;
-supports({array, bool})   -> true;
-supports({array, int2})   -> true;
-supports({array, int4})   -> true;
-supports({array, int8})   -> true;
-supports({array, float4}) -> true;
-supports({array, float8}) -> true;
-supports({array, char})   -> true;
-supports({array, text})   -> true;
-supports({array, date})   -> true;
-supports({array, time})   -> true;
-supports({array, timetz}) -> true;
-supports({array, timestamp})     -> true;
-supports({array, timestamptz})   -> true;
-supports({array, interval})      -> true;
-supports({array, hstore})        -> true;
-supports({array, varchar}) -> true;
-supports({array, uuid})   -> true;
-supports({array, cidr})   -> true;
-supports({array, inet})   -> true;
-supports({array, record}) -> true;
-supports({array, json})   -> true;
-supports({array, jsonb})   -> true;
-supports(int4range)       -> true;
-supports(int8range)       -> true;
-supports(_Type)       -> false.
+%% Supports
+supports(RecOid, _) when RecOid == ?RECORD_OID; RecOid == ?RECORD_ARRAY_OID ->
+    true;
+supports(Oid, #codec{oid_db = Db}) ->
+    epgsql_oid_db:find_by_oid(Oid, Db) =/= undefined.
+
+%% Default codec set
+%% XXX: maybe move to application env?
+-spec default_codecs() -> [{epgsql_codec:codec_mod(), any()}].
+default_codecs() ->
+    [{epgsql_codec_boolean, []},
+     {epgsql_codec_bpchar, []},
+     {epgsql_codec_datetime, []},
+     {epgsql_codec_float, []},
+     {epgsql_codec_geometric, []},
+     %% {epgsql_codec_hstore, []},
+     {epgsql_codec_integer, []},
+     {epgsql_codec_intrange, []},
+     {epgsql_codec_json, []},
+     {epgsql_codec_net, []},
+     %% {epgsql_codec_postgis,[]},
+     {epgsql_codec_text, []},
+     {epgsql_codec_timerange, []},
+     {epgsql_codec_uuid, []}
+    ].
+
+-spec default_oids() -> [epgsql_oid_db:oid_entry()].
+default_oids() ->
+    [{bool, 16, 1000},
+     {bpchar, 1042, 1014},
+     {bytea, 17, 1001},
+     {char, 18, 1002},
+     {cidr, 650, 651},
+     {date, 1082, 1182},
+     {daterange, 3912, 3913},
+     {float4, 700, 1021},
+     {float8, 701, 1022},
+     %% {geometry, 17063, 17071},
+     %% {hstore, 16935, 16940},
+     {inet, 869, 1041},
+     {int2, 21, 1005},
+     {int4, 23, 1007},
+     {int4range, 3904, 3905},
+     {int8, 20, 1016},
+     {int8range, 3926, 3927},
+     {interval, 1186, 1187},
+     {json, 114, 199},
+     {jsonb, 3802, 3807},
+     {macaddr, 829, 1040},
+     {macaddr8, 774, 775},
+     {point, 600, 1017},
+     {text, 25, 1009},
+     {time, 1083, 1183},
+     {timestamp, 1114, 1115},
+     {timestamptz, 1184, 1185},
+     {timetz, 1266, 1270},
+     {tsrange, 3908, 3909},
+     {tstzrange, 3910, 3911},
+     {uuid, 2950, 2951},
+     {varchar, 1043, 1015}
+    ].

+ 81 - 0
src/epgsql_codec.erl

@@ -0,0 +1,81 @@
+%%% @doc
+%%% Behaviour for postgresql datatype codecs.
+%%% XXX: this module and callbacks "know nothing" about OIDs.
+%%% 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.
+%%% @end
+%%% Created : 12 Oct 2017 by Sergey Prokhorov <me@seriyps.ru>
+
+-module(epgsql_codec).
+-export([init_mods/2, encode/4, decode/4, decode_text/4]).
+
+-export_type([codec_state/0, codec_mod/0, codec_entry/0]).
+
+%%
+%% Behaviour
+%%
+-type codec_state() :: any().
+-type codec_mod() :: module().
+
+-optional_callbacks([decode_text/3]).
+
+%% Called on connection start-up
+-callback init(any(), epgsql_sock:pg_sock()) -> codec_state().
+
+%% List of supported type names
+-callback names() -> [epgsql:type_name()].
+
+%% Encode Erlang representation to PG binary
+%% Called for each parameter, binary protocol (equery)
+-callback encode(Cell :: any(), epgsql:type_name(), codec_state()) -> iodata().
+
+%% Decode PG binary to erlang representation
+%% Called for each cell in each row, binary protocol (equery)
+-callback decode(Cell :: binary(), epgsql:type_name(), codec_state()) -> any().
+
+%% Decode PG string representation (text protocol) to erlang term.
+%% Called for each cell in each row, text protocol (squery)
+-callback decode_text(Cell :: binary(), epgsql:type_name(), codec_state()) ->
+    any().
+
+%% ==========
+-type codec_entry() :: {epgsql:type_name(),
+                        Mod :: codec_mod(),
+                        CallbackState :: any()}.
+
+-spec init_mods([{codec_mod(), any()}], epgsql_sock:pg_sock()) ->
+                       ordsets:ordset(codec_entry()).
+init_mods(Codecs, PgSock) ->
+    ModState = [{Mod, Mod:init(Opts, PgSock)} || {Mod, Opts} <- Codecs],
+    build_mapping(ModState, sets:new(), []).
+
+build_mapping([{Mod, _State} = MS | ModStates], Set, Acc) ->
+    Names = Mod:names(),
+    {Set1, Acc1} = add_names(Names, MS, Set, Acc),
+    build_mapping(ModStates, Set1, Acc1);
+build_mapping([], _, Acc) ->
+    ordsets:from_list(Acc).
+
+add_names([Name | Names], {Mod, State} = MS, Set, Acc) ->
+    case sets:is_element(Name, Set) of
+        true ->
+            add_names(Names, MS, Set, Acc);
+        false ->
+            Set1 = sets:add_element(Name, Set),
+            Acc1 = [{Name, Mod, State} | Acc],
+            add_names(Names, MS, Set1, Acc1)
+    end;
+add_names([], _, Set, Acc) ->
+    {Set, Acc}.
+
+-spec encode(codec_mod(), any(), epgsql:type_name(), codec_state()) -> iodata().
+encode(Mod, Cell, TypeName, CodecState) ->
+    Mod:encode(Cell, TypeName, CodecState).
+
+-spec decode(codec_mod(), binary(), epgsql:type_name(), codec_state()) -> any().
+decode(Mod, Cell, TypeName, CodecState) ->
+    Mod:decode(Cell, TypeName, CodecState).
+
+-spec decode_text(codec_mod(), binary(), epgsql:type_name(), codec_state()) -> any().
+decode_text(Mod, Cell, TypeName, CodecState) ->
+    Mod:decode(Cell, TypeName, CodecState).

+ 65 - 0
src/epgsql_command.erl

@@ -0,0 +1,65 @@
+%%% Behaviour module for epgsql_sock commands.
+%%%
+%%% Copyright (C) 2017 - Sergey Prokhorov.  All rights reserved.
+
+-module(epgsql_command).
+-export([init/2, execute/3, handle_message/5]).
+
+-export_type([command/0, state/0]).
+
+-type command() :: module().
+-type state() :: any().
+
+%% Initialize command's state. Called when command is received by epgsql_sock process.
+-callback init(any()) -> state().
+
+-type execute_return() ::
+        {ok, epgsql_sock:pg_sock(), state()}
+      | {stop, Reason :: any(), Response :: any(), epgsql_sock:pg_sock()}.
+%% Execute command. It should send commands to socket.
+%% May be called many times if 'handle_message' will return 'requeue'.
+-callback execute(epgsql_sock:pg_sock(), state()) -> execute_return().
+
+-type handle_message_return() ::
+        {noaction, epgsql_sock:pg_sock()}
+        %% Do nothing; remember changed state
+      | {noaction, epgsql_sock:pg_sock(), state()}
+        %% Add result to resultset (eg, `{ok, Count}' `{ok, Cols, Rows}', `{error, #error{}}'
+        %% It may be returned many times for eg, `squery' with multiple
+        %% queries separated by ';'
+        %% See epgsql_sock:get_results/1
+      | {add_result, Data :: any(), Notification :: any(), epgsql_sock:pg_sock(), state()}
+        %% Add new row to current resultset;
+        %% See epgsql_sock:get_rows/1
+      | {add_row, tuple(), epgsql_sock:pg_sock(), state()}
+        %% Finish command execution, reply to the client and go to next command
+      | {finish, Result :: any(), Notification :: any(), epgsql_sock:pg_sock()}
+        %% Stop `epgsql_sock' process
+      | {stop, Reason :: any(), Response :: any(), epgsql_sock:pg_sock()}
+        %% Call 'execute' and reschedule command.
+        %% It's forbidden to call epgsql_sock:send from `handle_message'.
+        %% If you need to do so, you should set some flag in state and
+        %% reschedule command.
+        %% See `epgsql_cmd_connect' for reference.
+      | {requeue, epgsql_sock:pg_sock(), state()}
+        %% Protocol synchronization error (eg, unexpected packet)
+        %% Drop command queue and don't accept any command except 'sync'
+      | {sync_required, Why :: any()}
+        %% Unknown packet. Terminate `epgsql_sock' process
+      | unknown.
+%% Handle incoming packet
+-callback handle_message(Type :: byte(), Payload :: binary() | epgsql:query_error(),
+                         epgsql_sock:pg_sock(), state()) -> handle_message_return().
+
+-spec init(command(), any()) -> state().
+init(Command, Args) ->
+    Command:init(Args).
+
+-spec execute(command(), epgsql_sock:pg_sock(), state()) -> execute_return().
+execute(Command, PgSock, CmdState) ->
+    Command:execute(PgSock, CmdState).
+
+-spec handle_message(command(), Type :: byte(), Payload :: binary() | epgsql:query_error(),
+                     epgsql_sock:pg_sock(), state()) -> handle_message_return().
+handle_message(Command, Type, Payload, PgSock, State) ->
+    Command:handle_message(Type, Payload, PgSock, State).

+ 8 - 1
src/epgsql_errcodes.erl

@@ -1,4 +1,4 @@
-%% DO NOT EDIT - AUTOGENERATED ON Mon  1 Jun 2015 14:42:37 BST
+%% DO NOT EDIT - AUTOGENERATED BY ./generate_errcodes_src.sh ON 2018-02-23T11:18:01+0100
 -module(epgsql_errcodes).
 -export([to_name/1]).
 
@@ -55,15 +55,19 @@ to_name(<<"22025">>) -> invalid_escape_sequence;
 to_name(<<"22P06">>) -> nonstandard_use_of_escape_character;
 to_name(<<"22010">>) -> invalid_indicator_parameter_value;
 to_name(<<"22023">>) -> invalid_parameter_value;
+to_name(<<"22013">>) -> invalid_preceding_following_size;
 to_name(<<"2201B">>) -> invalid_regular_expression;
 to_name(<<"2201W">>) -> invalid_row_count_in_limit_clause;
 to_name(<<"2201X">>) -> invalid_row_count_in_result_offset_clause;
+to_name(<<"2202H">>) -> invalid_tablesample_argument;
+to_name(<<"2202G">>) -> invalid_tablesample_repeat;
 to_name(<<"22009">>) -> invalid_time_zone_displacement_value;
 to_name(<<"2200C">>) -> invalid_use_of_escape_character;
 to_name(<<"2200G">>) -> most_specific_type_mismatch;
 to_name(<<"22004">>) -> null_value_not_allowed;
 to_name(<<"22002">>) -> null_value_no_indicator_parameter;
 to_name(<<"22003">>) -> numeric_value_out_of_range;
+to_name(<<"2200H">>) -> sequence_generator_limit_exceeded;
 to_name(<<"22026">>) -> string_data_length_mismatch;
 to_name(<<"22001">>) -> string_data_right_truncation;
 to_name(<<"22011">>) -> substring_error;
@@ -99,6 +103,7 @@ to_name(<<"25006">>) -> read_only_sql_transaction;
 to_name(<<"25007">>) -> schema_and_data_statement_mixing_not_supported;
 to_name(<<"25P01">>) -> no_active_sql_transaction;
 to_name(<<"25P02">>) -> in_failed_sql_transaction;
+to_name(<<"25P03">>) -> idle_in_transaction_session_timeout;
 to_name(<<"26000">>) -> invalid_sql_statement_name;
 to_name(<<"27000">>) -> triggered_data_change_violation;
 to_name(<<"28000">>) -> invalid_authorization_specification;
@@ -148,6 +153,7 @@ to_name(<<"42P18">>) -> indeterminate_datatype;
 to_name(<<"42P21">>) -> collation_mismatch;
 to_name(<<"42P22">>) -> indeterminate_collation;
 to_name(<<"42809">>) -> wrong_object_type;
+to_name(<<"428C9">>) -> generated_always;
 to_name(<<"42703">>) -> undefined_column;
 to_name(<<"42883">>) -> undefined_function;
 to_name(<<"42P01">>) -> undefined_table;
@@ -199,6 +205,7 @@ to_name(<<"58000">>) -> system_error;
 to_name(<<"58030">>) -> io_error;
 to_name(<<"58P01">>) -> undefined_file;
 to_name(<<"58P02">>) -> duplicate_file;
+to_name(<<"72000">>) -> snapshot_too_old;
 to_name(<<"F0000">>) -> config_file_error;
 to_name(<<"F0001">>) -> lock_file_exists;
 to_name(<<"HV000">>) -> fdw_error;

+ 27 - 63
src/epgsql_fdatetime.erl

@@ -4,71 +4,35 @@
 
 -export([decode/2, encode/2]).
 
--include("epgsql_binary.hrl").
+-include("protocol.hrl").
 
--define(postgres_epoc_jdate, 2451545).
--define(postgres_epoc_secs, 946684800).
+-define(POSTGRES_EPOC_JDATE, 2451545).
+-define(POSTGRES_EPOC_SECS, 946684800).
 
--define(mins_per_hour, 60).
--define(secs_per_day, 86400.0).
--define(secs_per_hour, 3600.0).
--define(secs_per_minute, 60.0).
+-define(MINS_PER_HOUR, 60).
+-define(SECS_PER_DAY, 86400.0).
+-define(SECS_PER_HOUR, 3600.0).
+-define(SECS_PER_MINUTE, 60.0).
 
-decode(date, <<J:1/big-signed-unit:32>>)             -> j2date(?postgres_epoc_jdate + J);
+decode(date, <<J:1/big-signed-unit:32>>)             -> epgsql_idatetime:j2date(?POSTGRES_EPOC_JDATE + J);
 decode(time, <<N:1/big-float-unit:64>>)              -> f2time(N);
 decode(timetz, <<N:1/big-float-unit:64, TZ:?int32>>) -> {f2time(N), TZ};
 decode(timestamp, <<N:1/big-float-unit:64>>)         -> f2timestamp(N);
 decode(timestamptz, <<N:1/big-float-unit:64>>)       -> f2timestamp(N);
 decode(interval, <<N:1/big-float-unit:64, D:?int32, M:?int32>>) -> {f2time(N), D, M}.
 
-encode(date, D)         -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):1/big-signed-unit:32>>;
-encode(time, T)         -> <<8:?int32, (time2f(T)):1/big-float-unit:64>>;
-encode(timetz, {T, TZ}) -> <<12:?int32, (time2f(T)):1/big-float-unit:64, TZ:?int32>>;
-encode(timestamp, TS = {_, _, _})   -> <<8:?int32, (now2f(TS)):1/big-float-unit:64>>;
-encode(timestamp, TS)   -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>;
-encode(timestamptz, TS = {_, _, _})   -> <<8:?int32, (now2f(TS)):1/big-float-unit:64>>;
-encode(timestamptz, TS) -> <<8:?int32, (timestamp2f(TS)):1/big-float-unit:64>>;
-encode(interval, {T, D, M}) -> <<16:?int32, (time2f(T)):1/big-float-unit:64, D:?int32, M:?int32>>.
-
-j2date(N) ->
-    J = N + 32044,
-    Q1 = J div 146097,
-    Extra = (J - Q1 * 146097) * 4 + 3,
-    J2 = J + 60 + Q1 * 3 + Extra div 146097,
-    Q2 = J2 div 1461,
-    J3 = J2 - Q2 * 1461,
-    Y = J3 * 4 div 1461,
-    J4 = case Y of
-        0 -> ((J3 + 306) rem 366) + 123;
-        _ -> ((J3 + 305) rem 365) + 123
-    end,
-    Year = (Y + Q2 * 4) - 4800,
-    Q3 = J4 * 2141 div 65536,
-    Day = J4 - 7834 * Q3 div 256,
-    Month = (Q3 + 10) rem 12 + 1,
-    {Year, Month, Day}.
-
-date2j({Y, M, D}) ->
-    M2 = case M > 2 of
-        true ->
-            M + 1;
-        false ->
-            M + 13
-    end,
-    Y2 = case M > 2 of
-        true ->
-            Y + 4800;
-        false ->
-            Y + 4799
-    end,
-    C = Y2 div 100,
-    J1 = Y2 * 365 - 32167,
-    J2 = J1 + (Y2 div 4 - C + C div 4),
-    J2 + 7834 * M2 div 256 + D.
+encode(date, D)         -> <<(epgsql_idatetime:date2j(D) - ?POSTGRES_EPOC_JDATE):1/big-signed-unit:32>>;
+encode(time, T)         -> <<(time2f(T)):1/big-float-unit:64>>;
+encode(timetz, {T, TZ}) -> <<(time2f(T)):1/big-float-unit:64, TZ:?int32>>;
+encode(timestamp, TS = {_, _, _})   -> <<(now2f(TS)):1/big-float-unit:64>>;
+encode(timestamp, TS)   -> <<(timestamp2f(TS)):1/big-float-unit:64>>;
+encode(timestamptz, TS = {_, _, _})   -> <<(now2f(TS)):1/big-float-unit:64>>;
+encode(timestamptz, TS) -> <<(timestamp2f(TS)):1/big-float-unit:64>>;
+encode(interval, {T, D, M}) -> <<(time2f(T)):1/big-float-unit:64, D:?int32, M:?int32>>.
 
 f2time(N) ->
-    {R1, Hour} = tmodulo(N, ?secs_per_hour),
-    {R2, Min}  = tmodulo(R1, ?secs_per_minute),
+    {R1, Hour} = tmodulo(N, ?SECS_PER_HOUR),
+    {R2, Min}  = tmodulo(R1, ?SECS_PER_MINUTE),
     {R3, Sec}  = tmodulo(R2, 1.0),
     case timeround(R3) of
         US when US >= 1.0 -> f2time(ceiling(N));
@@ -76,21 +40,21 @@ f2time(N) ->
     end.
 
 time2f({H, M, S}) ->
-    ((H * ?mins_per_hour + M) * ?secs_per_minute) + S.
+    ((H * ?MINS_PER_HOUR + M) * ?SECS_PER_MINUTE) + S.
 
 f2timestamp(N) ->
-    case tmodulo(N, ?secs_per_day) of
-        {T, D} when T < 0 -> f2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?secs_per_day);
-        {T, D}            -> f2timestamp2(D + ?postgres_epoc_jdate, T)
+    case tmodulo(N, ?SECS_PER_DAY) of
+        {T, D} when T < 0 -> f2timestamp2(D - 1 + ?POSTGRES_EPOC_JDATE, T + ?SECS_PER_DAY);
+        {T, D}            -> f2timestamp2(D + ?POSTGRES_EPOC_JDATE, T)
     end.
 
 f2timestamp2(D, T) ->
     {_H, _M, S} = Time = f2time(T),
-    Date = j2date(D),
+    Date = epgsql_idatetime:j2date(D),
     case tsround(S - trunc(S)) of
         N when N >= 1.0 ->
             case ceiling(T) of
-                T2 when T2 > ?secs_per_day -> f2timestamp2(D + 1, 0.0);
+                T2 when T2 > ?SECS_PER_DAY -> f2timestamp2(D + 1, 0.0);
                 T2                         -> f2timestamp2(T2, D)
             end;
         _ -> ok
@@ -98,11 +62,11 @@ f2timestamp2(D, T) ->
     {Date, Time}.
 
 timestamp2f({Date, Time}) ->
-    D = date2j(Date) - ?postgres_epoc_jdate,
-    D * ?secs_per_day + time2f(Time).
+    D = epgsql_idatetime:date2j(Date) - ?POSTGRES_EPOC_JDATE,
+    D * ?SECS_PER_DAY + time2f(Time).
 
 now2f({MegaSecs, Secs, MicroSecs}) ->
-    MegaSecs * 1000000 + Secs + MicroSecs / 1000000.0 - ?postgres_epoc_secs.
+    MegaSecs * 1000000 + Secs + MicroSecs / 1000000.0 - ?POSTGRES_EPOC_SECS.
 
 tmodulo(T, U) ->
     Q = case T < 0 of

+ 37 - 34
src/epgsql_idatetime.erl

@@ -3,36 +3,39 @@
 -module(epgsql_idatetime).
 
 -export([decode/2, encode/2]).
+-export([j2date/1, date2j/1]).
 
--include("epgsql_binary.hrl").
+-include("protocol.hrl").
 
--define(postgres_epoc_jdate, 2451545).
--define(postgres_epoc_usecs, 946684800000000).
+-define(POSTGRES_EPOC_JDATE, 2451545).
+-define(POSTGRES_EPOC_USECS, 946684800000000).
 
--define(mins_per_hour, 60).
--define(secs_per_minute, 60).
+-define(MINS_PER_HOUR, 60).
+-define(SECS_PER_MINUTE, 60).
 
--define(usecs_per_day, 86400000000).
--define(usecs_per_hour, 3600000000).
--define(usecs_per_minute, 60000000).
--define(usecs_per_sec, 1000000).
+-define(USECS_PER_DAY, 86400000000).
+-define(USECS_PER_HOUR, 3600000000).
+-define(USECS_PER_MINUTE, 60000000).
+-define(USECS_PER_SEC, 1000000).
 
-decode(date, <<J:?int32>>)                         -> j2date(?postgres_epoc_jdate + J);
+decode(date, <<J:?int32>>)                         -> j2date(?POSTGRES_EPOC_JDATE + J);
 decode(time, <<N:?int64>>)                         -> i2time(N);
 decode(timetz, <<N:?int64, TZ:?int32>>)            -> {i2time(N), TZ};
 decode(timestamp, <<N:?int64>>)                    -> i2timestamp(N);
 decode(timestamptz, <<N:?int64>>)                  -> i2timestamp(N);
 decode(interval, <<N:?int64, D:?int32, M:?int32>>) -> {i2time(N), D, M}.
 
-encode(date, D)         -> <<4:?int32, (date2j(D) - ?postgres_epoc_jdate):?int32>>;
-encode(time, T)         -> <<8:?int32, (time2i(T)):?int64>>;
-encode(timetz, {T, TZ}) -> <<12:?int32, (time2i(T)):?int64, TZ:?int32>>;
-encode(timestamp, TS = {_, _, _})   -> <<8:?int32, (now2i(TS)):?int64>>;
-encode(timestamp, TS)   -> <<8:?int32, (timestamp2i(TS)):?int64>>;
-encode(timestamptz, TS = {_, _, _})   -> <<8:?int32, (now2i(TS)):?int64>>;
-encode(timestamptz, TS) -> <<8:?int32, (timestamp2i(TS)):?int64>>;
-encode(interval, {T, D, M}) -> <<16:?int32, (time2i(T)):?int64, D:?int32, M:?int32>>.
-
+encode(date, D)         -> <<(date2j(D) - ?POSTGRES_EPOC_JDATE):?int32>>;
+encode(time, T)         -> <<(time2i(T)):?int64>>;
+encode(timetz, {T, TZ}) -> <<(time2i(T)):?int64, TZ:?int32>>;
+encode(timestamp, TS = {_, _, _})   -> <<(now2i(TS)):?int64>>;
+encode(timestamp, TS)   -> <<(timestamp2i(TS)):?int64>>;
+encode(timestamptz, TS = {_, _, _})   -> <<(now2i(TS)):?int64>>;
+encode(timestamptz, TS) -> <<(timestamp2i(TS)):?int64>>;
+encode(interval, {T, D, M}) -> <<(time2i(T)):?int64, D:?int32, M:?int32>>.
+
+%% Julian calendar
+%% See $PG$/src/backend/utils/adt/datetime.c
 j2date(N) ->
     J = N + 32044,
     Q1 = J div 146097,
@@ -70,33 +73,33 @@ date2j({Y, M, D}) ->
     J2 + 7834 * M2 div 256 + D.
 
 i2time(N) ->
-    Hour = N div ?usecs_per_hour,
-    R1 = N - Hour * ?usecs_per_hour,
-    Min = R1 div ?usecs_per_minute,
-    R2 = R1 - Min * ?usecs_per_minute,
-    Sec = R2 div ?usecs_per_sec,
-    US = R2 - Sec * ?usecs_per_sec,
-    {Hour, Min, Sec + US / ?usecs_per_sec}.
+    Hour = N div ?USECS_PER_HOUR,
+    R1 = N - Hour * ?USECS_PER_HOUR,
+    Min = R1 div ?USECS_PER_MINUTE,
+    R2 = R1 - Min * ?USECS_PER_MINUTE,
+    Sec = R2 div ?USECS_PER_SEC,
+    US = R2 - Sec * ?USECS_PER_SEC,
+    {Hour, Min, Sec + US / ?USECS_PER_SEC}.
 
 time2i({H, M, S}) ->
-    US = trunc(round(S * ?usecs_per_sec)),
-    ((H * ?mins_per_hour + M) * ?secs_per_minute) * ?usecs_per_sec + US.
+    US = trunc(round(S * ?USECS_PER_SEC)),
+    ((H * ?MINS_PER_HOUR + M) * ?SECS_PER_MINUTE) * ?USECS_PER_SEC + US.
 
 i2timestamp(N) ->
-    case tmodulo(N, ?usecs_per_day) of
-        {T, D} when T < 0 -> i2timestamp2(D - 1 + ?postgres_epoc_jdate, T + ?usecs_per_day);
-        {T, D}            -> i2timestamp2(D + ?postgres_epoc_jdate, T)
+    case tmodulo(N, ?USECS_PER_DAY) of
+        {T, D} when T < 0 -> i2timestamp2(D - 1 + ?POSTGRES_EPOC_JDATE, T + ?USECS_PER_DAY);
+        {T, D}            -> i2timestamp2(D + ?POSTGRES_EPOC_JDATE, T)
     end.
 
 i2timestamp2(D, T) ->
     {j2date(D), i2time(T)}.
 
 timestamp2i({Date, Time}) ->
-    D = date2j(Date) - ?postgres_epoc_jdate,
-    D * ?usecs_per_day + time2i(Time).
+    D = date2j(Date) - ?POSTGRES_EPOC_JDATE,
+    D * ?USECS_PER_DAY + time2i(Time).
 
 now2i({MegaSecs, Secs, MicroSecs}) ->
-    (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs - ?postgres_epoc_usecs.
+    (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs - ?POSTGRES_EPOC_USECS.
 
 tmodulo(T, U) ->
     case T div U of

+ 195 - 0
src/epgsql_oid_db.erl

@@ -0,0 +1,195 @@
+%%% @author Sergey Prokhorov <me@seriyps.ru>
+%%% @doc
+%%% Holds Oid <-> Type mappings (forward and reverse).
+%%% See https://www.postgresql.org/docs/current/static/catalog-pg-type.html
+%%% @end
+
+-module(epgsql_oid_db).
+
+-export([build_query/1, parse_rows/1, join_codecs_oids/2]).
+-export([from_list/1, to_list/1, update/2,
+         find_by_oid/2, find_by_name/3, oid_by_name/3,
+         type_to_codec_entry/1, type_to_oid_info/1, type_to_element_oid/1]).
+-export_type([oid/0, oid_info/0, oid_entry/0, type_info/0, db/0]).
+
+-record(type,
+        {oid :: oid(),
+         name :: epgsql:type_name(),
+         is_array :: boolean(),
+         array_element_oid :: oid() | undefined,
+         codec :: module(),
+         codec_state :: any()}).
+-record(oid_db,
+        {by_oid :: kv(oid(), #type{}),
+         by_name :: kv({epgsql:type_name(), boolean()}, oid())}).
+
+-type oid() :: non_neg_integer().
+%% Row of `typname', `oid', `typarray' from pg_type table.
+-type oid_entry() :: {epgsql:type_name(), Oid :: oid(), ArrayOid :: oid()}.
+-type oid_info() :: {Oid :: oid(), epgsql:type_name(), IsArray :: boolean()}.
+-opaque db() :: #oid_db{}.
+-opaque type_info() :: #type{}.
+
+
+%%
+%% pg_type Data preparation
+%%
+
+%% @doc build query to fetch OID<->type_name information from PG server
+-spec build_query([epgsql:type_name() | binary()]) -> iolist().
+build_query(TypeNames) ->
+    %% TODO: lists:join/2, ERL 19+
+    %% XXX: we don't escape type names!
+    ToBin = fun(B) when is_binary(B) -> B;
+               (A) when is_atom(A) -> atom_to_binary(A, utf8)
+            end,
+    Types = join(",",
+                 [["'", ToBin(TypeName) | "'"]
+                  || TypeName <- TypeNames]),
+    [<<"SELECT typname::text, oid::int4, typarray::int4 "
+       "FROM pg_type "
+       "WHERE typname IN (">>, Types, <<") ORDER BY typname">>].
+
+%% @doc Parse result of `squery(build_query(...))'
+-spec parse_rows(ordsets:ordset({binary(), binary(), binary()})) ->
+                        ordsets:ordset(oid_entry()).
+parse_rows(Rows) ->
+    [{binary_to_existing_atom(TypeName, utf8),
+      binary_to_integer(Oid),
+      binary_to_integer(ArrayOid)}
+     || {TypeName, Oid, ArrayOid} <- Rows].
+
+%% @doc Build list of #type{}'s by merging oid and codec lists by type name.
+-spec join_codecs_oids(ordsets:ordset(oid_entry()),
+                       ordsets:ordset(epgsql_codec:codec_entry())) -> [type_info()].
+join_codecs_oids(Oids, Codecs) ->
+    do_join(lists:sort(Oids), lists:sort(Codecs)).
+
+do_join([{TypeName, Oid, ArrayOid} | Oids],
+        [{TypeName, CallbackMod, CallbackState} | Codecs]) ->
+    [#type{oid = Oid, name = TypeName, is_array = false,
+           codec = CallbackMod, codec_state = CallbackState},
+     #type{oid = ArrayOid, name = TypeName, is_array = true,
+           codec = CallbackMod, codec_state = CallbackState,
+           array_element_oid = Oid}
+     | do_join(Oids, Codecs)];
+do_join([OidEntry | _Oids] = Oids, [CodecEntry | Codecs])
+  when element(1, OidEntry) > element(1, CodecEntry) ->
+    %% This type isn't supported by PG server. That's ok, but not vice-versa.
+    do_join(Oids, Codecs);
+do_join([], _) ->
+    %% Codecs list may be not empty. See prev clause.
+    [].
+
+
+%%
+%% Storage API
+%%
+
+-spec from_list([type_info()]) -> db().
+from_list(Types) ->
+    #oid_db{by_oid = kv_from_list(
+                       [{Oid, Type} || #type{oid = Oid} = Type <- Types]),
+            by_name = kv_from_list(
+                        [{{Name, IsArray}, Oid}
+                         || #type{name = Name, is_array = IsArray, oid = Oid}
+                                <- Types])}.
+
+to_list(#oid_db{by_oid = Dict}) ->
+    [Type || {_Oid, Type} <- kv_to_list(Dict)].
+
+%% @doc update DB adding new type definitions.
+%% If some of type definitions already exist, old ones will be overwritten by new ones
+-spec update([type_info()], db()) -> db().
+update(Types, #oid_db{by_oid = OldByOid, by_name = OldByName} = Store) ->
+    #oid_db{by_oid = NewByOid, by_name = NewByName} = from_list(Types),
+    ByOid = kv_merge(OldByOid, NewByOid),
+    ByName = kv_merge(OldByName, NewByName),
+    Store#oid_db{by_oid = ByOid,
+                 by_name = ByName}.
+
+%% @doc find type by OID
+-spec find_by_oid(oid(), db()) -> type_info() | undefined.
+find_by_oid(Oid, #oid_db{by_oid = Dict}) ->
+    kv_get(Oid, Dict, undefined).
+
+%% @doc find type by type name
+-spec find_by_name(epgsql:type_name(), boolean(), db()) -> type_info().
+find_by_name(Name, IsArray, #oid_db{by_oid = ByOid} = Db) ->
+    Oid = oid_by_name(Name, IsArray, Db),
+    kv_get(Oid, ByOid).                  % or maybe find_by_oid(Oid, Store)
+
+%% @doc lookup OID by type name. May fall
+-spec oid_by_name(epgsql:type_name(), boolean(), db()) -> oid().
+oid_by_name(Name, IsArray, #oid_db{by_name = ByName}) ->
+    kv_get({Name, IsArray}, ByName).
+
+%% @doc convert type to codec_entry()
+-spec type_to_codec_entry(type_info()) -> epgsql_codec:codec_entry().
+type_to_codec_entry(#type{name = Name, codec = Codec, codec_state = State}) ->
+    {Name, Codec, State}.
+
+%% @doc Convert type tp oid_info()
+-spec type_to_oid_info(type_info()) -> oid_info().
+type_to_oid_info(#type{name = Name, is_array = IsArray, oid = Oid}) ->
+    {Oid, Name, IsArray}.
+
+%% @doc For array types return its element's OID
+-spec type_to_element_oid(type_info()) -> oid() | undefined.
+type_to_element_oid(#type{array_element_oid = ElementOid, is_array = true}) ->
+    ElementOid.
+
+%% Internal
+%% TODO: replace by Erlang >=19 lists:join/2
+join(_Sep, []) -> [];
+join(Sep, [H | T]) -> [H | join_prepend(Sep, T)].
+
+join_prepend(_Sep, []) -> [];
+join_prepend(Sep, [H | T]) -> [Sep, H | join_prepend(Sep, T)].
+
+
+%% K-V storage
+%% In Erlang 17 map access time is O(n), so, it's faster to use dicts.
+%% In Erlang >=18 maps are the most eficient choice
+-ifndef(SLOW_MAPS).
+
+-type kv(K, V) :: #{K => V}.
+
+kv_from_list(L) ->
+    maps:from_list(L).
+
+kv_to_list(Map) ->
+    maps:to_list(Map).
+
+kv_get(Key, Map) ->
+    maps:get(Key, Map).
+
+kv_get(Key, Map, Default) ->
+    maps:get(Key, Map, Default).
+
+kv_merge(Map1, Map2) ->
+    maps:merge(Map1, Map2).
+
+-else.
+
+-type kv(_K, _V) :: any(). % dict:dict(K, V) causes dialyzer errors on erl <= 17
+
+kv_from_list(L) ->
+    dict:from_list(L).
+
+kv_to_list(Dict) ->
+    dict:to_list(Dict).
+
+kv_get(Key, Dict) ->
+    dict:fetch(Key, Dict).
+
+kv_get(Key, Dict, Default) ->
+    case dict:find(Key, Dict) of
+        {ok, Value} -> Value;
+        error -> Default
+    end.
+
+kv_merge(Dict1, Dict2) ->
+    dict:merge(fun(_, _, V2) -> V2 end, Dict1, Dict2).
+
+-endif.

+ 11 - 0
src/epgsql_replication.hrl

@@ -0,0 +1,11 @@
+-record(repl,
+        {
+          last_received_lsn :: integer() | undefined,
+          last_flushed_lsn :: integer() | undefined,
+          last_applied_lsn :: integer() | undefined,
+          feedback_required :: boolean() | undefined,
+          cbmodule :: module() | undefined,
+          cbstate :: any() | undefined,
+          receiver :: pid() | undefined,
+          align_lsn :: integer() | undefined
+        }).

+ 172 - 0
src/epgsql_scram.erl

@@ -0,0 +1,172 @@
+%%% coding: utf-8
+%%% @doc
+%%% 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
+%%% @end
+
+-module(epgsql_scram).
+-export([get_nonce/1,
+         get_client_first/2,
+         get_client_final/4,
+         parse_server_first/2,
+         parse_server_final/1]).
+-export([hi/3,
+         hmac/2,
+         h/1,
+         bin_xor/2]).
+
+-type nonce() :: binary().
+-type server_first() :: [{nonce, nonce()} |
+                         {salt, binary()} |
+                         {i, pos_integer()} |
+                         {raw, binary()}].
+
+-spec get_client_first(iodata(), nonce()) -> iodata().
+get_client_first(UserName, Nonce) ->
+    %% Username is ignored by postgresql
+    [<<"n,,">> | client_first_bare(UserName, Nonce)].
+
+client_first_bare(UserName, Nonce) ->
+    [<<"n=">>, UserName, <<",r=">>, Nonce].
+
+%% @doc Generate unique ASCII string.
+%% Resulting string length isn't guaranteed, but it's guaranteed to be unique and will
+%% contain `NumRandomBytes' of random data.
+-spec get_nonce(pos_integer()) -> nonce().
+get_nonce(NumRandomBytes) when NumRandomBytes < 255 ->
+    Random = crypto:strong_rand_bytes(NumRandomBytes),
+    Unique = binary:encode_unsigned(unique()),
+    NonceBin = <<NumRandomBytes, Random:NumRandomBytes/binary, Unique/binary>>,
+    base64:encode(NonceBin).
+
+-spec parse_server_first(binary(), nonce()) -> server_first().
+parse_server_first(ServerFirst, ClientNonce) ->
+    PartsB = binary:split(ServerFirst, <<",">>, [global]),
+    (length(PartsB) == 3) orelse error({invalid_server_first, ServerFirst}),
+    Parts =
+        lists:map(
+          fun(<<"r=", R/binary>>) ->
+                  {nonce, R};
+             (<<"s=", S/binary>>) ->
+                  {salt, base64:decode(S)};
+             (<<"i=", I/binary>>) ->
+                  {i, binary_to_integer(I)}
+          end, PartsB),
+    check_nonce(ClientNonce, proplists:get_value(nonce, Parts)),
+    [{raw, ServerFirst} | Parts].
+
+%% SaltedPassword  := Hi(Normalize(password), salt, i)
+%% ClientKey       := HMAC(SaltedPassword, "Client Key")
+%% StoredKey       := H(ClientKey)
+%% AuthMessage     := client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
+%% ClientSignature := HMAC(StoredKey, AuthMessage)
+%% ClientProof     := ClientKey XOR ClientSignature
+-spec get_client_final(server_first(), nonce(), iodata(), iodata()) ->
+                              {ClientFinal :: iodata(), ServerSignature :: binary()}.
+get_client_final(SrvFirst, ClientNonce, UserName, Password) ->
+    ChannelBinding = <<"c=biws">>,                 %channel-binding isn't implemented
+    Nonce = [<<"r=">>, proplists:get_value(nonce, SrvFirst)],
+
+    Salt = proplists:get_value(salt, SrvFirst),
+    I = proplists:get_value(i, SrvFirst),
+
+    SaltedPassword = hi(normalize(Password), Salt, I),
+    ClientKey = hmac(SaltedPassword, "Client Key"),
+    StoredKey = h(ClientKey),
+    ClientFirstBare = client_first_bare(UserName, ClientNonce),
+    ServerFirst = proplists:get_value(raw, SrvFirst),
+    ClientFinalWithoutProof = [ChannelBinding, ",", Nonce],
+    AuthMessage = [ClientFirstBare, ",", ServerFirst, ",", ClientFinalWithoutProof],
+    ClientSignature = hmac(StoredKey, AuthMessage),
+    ClientProof = bin_xor(ClientKey, ClientSignature),
+
+    ServerKey = hmac(SaltedPassword, "Server Key"),
+    ServerSignature = hmac(ServerKey, AuthMessage),
+
+    {[ClientFinalWithoutProof, ",p=", base64:encode(ClientProof)], ServerSignature}.
+
+-spec parse_server_final(binary()) -> {ok, binary()} | {error, binary()}.
+parse_server_final(<<"v=", ServerFinal/binary>>) ->
+    [ServerFinal1 | _] = binary:split(ServerFinal, <<",">>),
+    {ok, base64:decode(ServerFinal1)};
+parse_server_final(<<"e=", ServerError/binary>>) ->
+    {error, ServerError}.
+
+%% Helpers
+
+%% TODO: implement according to rfc3454
+normalize(Str) ->
+    lists:all(fun is_ascii_non_control/1, unicode:characters_to_list(Str, utf8))
+        orelse error({scram_non_ascii_password, Str}),
+    Str.
+
+is_ascii_non_control(C) when C > 16#1F, C < 16#7F -> true;
+is_ascii_non_control(_) -> false.
+
+check_nonce(ClientNonce, ServerNonce) ->
+    Size = size(ClientNonce),
+    <<ClientNonce:Size/binary, _/binary>> = ServerNonce,
+    true.
+
+hi(Str, Salt, I) ->
+    U1 = hmac(Str, <<Salt/binary, 1:32/integer-big>>),
+    hi1(Str, U1, U1, I - 1).
+
+hi1(_Str, _U, Hi, 0) ->
+    Hi;
+hi1(Str, U, Hi, I) ->
+    U2 = hmac(Str, U),
+    Hi1 = bin_xor(Hi, U2),
+    hi1(Str, U2, Hi1, I - 1).
+
+hmac(Key, Str) ->
+    crypto:hmac(sha256, Key, Str).
+
+h(Str) ->
+    crypto:hash(sha256, Str).
+
+%% word 'xor' is reserved
+bin_xor(B1, B2) ->
+    crypto:exor(B1, B2).
+
+
+-ifndef(SLOW_MAPS).
+unique() ->
+    erlang:unique_integer([positive]).
+-else.
+unique() ->
+    %% POSIX timestamp microseconds
+    {Mega, Secs, Micro} = erlang:now(),
+    (Mega * 1000000 + Secs) * 1000000 + Micro.
+-endif.
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+exchange_test() ->
+    Password = <<"foobar">>,
+    Nonce = <<"9IZ2O01zb9IgiIZ1WJ/zgpJB">>,
+    Username = <<>>,
+
+    ClientFirst = <<"n,,n=,r=9IZ2O01zb9IgiIZ1WJ/zgpJB">>,
+    ServerFirst = <<"r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i=4096">>,
+    ClientFinal = <<"c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS31NTlQ"
+                    "YNs5BTeQjdHdk7lOflDo5re2an8=">>,
+    ServerFinal = <<"v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw=">>,
+
+    ?assertEqual(ClientFirst, iolist_to_binary(get_client_first(Username, Nonce))),
+    SF = parse_server_first(ServerFirst, Nonce),
+    {CF, ServerProof} = get_client_final(SF, Nonce, Username, Password),
+    ?assertEqual(ClientFinal, iolist_to_binary(CF)),
+    ?assertEqual({ok, ServerProof}, parse_server_final(ServerFinal)).
+
+normalize_test() ->
+    ?assertEqual(<<"123 !~">>, normalize(<<"123 !~">>)),
+    ?assertError({scram_non_ascii_password, _}, normalize(<<"привет"/utf8>>)).
+
+-endif.

+ 360 - 652
src/epgsql_sock.erl

@@ -1,12 +1,34 @@
 %%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
 %%% Copyright (C) 2011 - Anton Lebedevich.  All rights reserved.
 
+%%% @doc GenServer holding all 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.
+%%% 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
+%%% packets for #1 and then all for #2:
+%%% > SQuery #1
+%%% > SQuery #2
+%%% < RowDescription #1
+%%% < DataRow #1
+%%% < CommandComplete #1
+%%% < RowDescription #2
+%%% < DataRow #2
+%%% < CommandComplete #2
+%%%
+%%% See epgsql_cmd_connect for network connection and authentication setup
+
+
 -module(epgsql_sock).
 
 -behavior(gen_server).
 
 -export([start_link/0,
          close/1,
+         sync_command/3,
+         async_command/4,
          get_parameter/2,
          set_notice_receiver/2,
          get_cmd_status/1,
@@ -15,82 +37,48 @@
 -export([handle_call/3, handle_cast/2, handle_info/2]).
 -export([init/1, code_change/3, terminate/2]).
 
-%% state callbacks
--export([auth/2, initializing/2, on_message/2]).
+%% loop callback
+-export([on_message/3, on_replication/3]).
+
+%% Comand's APIs
+-export([set_net_socket/3, init_replication_state/1, set_attr/3, get_codec/1,
+         get_rows/1, get_results/1, notify/2, send/2, send/3, send_multi/2,
+         get_parameter_internal/2,
+         get_replication_state/1, set_packet_handler/2]).
+
+-export_type([transport/0, pg_sock/0]).
 
 -include("epgsql.hrl").
--include("epgsql_binary.hrl").
-
-%% Commands defined as per this page:
-%% http://www.postgresql.org/docs/9.2/static/protocol-message-formats.html
-
-%% Commands
--define(BIND, $B).
--define(CLOSE, $C).
--define(DESCRIBE, $D).
--define(EXECUTE, $E).
--define(FLUSH, $H).
--define(PASSWORD, $p).
--define(PARSE, $P).
--define(SIMPLEQUERY, $Q).
--define(AUTHENTICATION_REQUEST, $R).
--define(SYNC, $S).
-
-%% Parameters
-
--define(PREPARED_STATEMENT, $S).
--define(PORTAL, $P).
-
-%% Responses
-
--define(PARSE_COMPLETE, $1).
--define(BIND_COMPLETE, $2).
--define(CLOSE_COMPLETE, $3).
--define(NOTIFICATION, $A).
--define(COMMAND_COMPLETE, $C).
--define(DATA_ROW, $D).
--define(EMPTY_QUERY, $I).
--define(CANCELLATION_KEY, $K).
--define(NO_DATA, $n).
--define(NOTICE, $N).
--define(PORTAL_SUSPENDED, $s).
--define(PARAMETER_STATUS, $S).
--define(PARAMETER_DESCRIPTION, $t).
--define(ROW_DESCRIPTION, $T).
--define(READY_FOR_QUERY, $Z).
--define(COPY_BOTH_RESPONSE, $W).
--define(COPY_DATA, $d).
-
-% CopyData replication messages
--define(X_LOG_DATA, $w).
--define(PRIMARY_KEEPALIVE_MESSAGE, $k).
--define(STANDBY_STATUS_UPDATE, $r).
-
--record(state, {mod,
-                sock,
+-include("protocol.hrl").
+-include("epgsql_replication.hrl").
+
+-type transport() :: {call, any()}
+                   | {cast, pid(), reference()}
+                   | {incremental, pid(), reference()}.
+
+-type tcp_socket() :: port(). %gen_tcp:socket() isn't exported prior to erl 18
+-type repl_state() :: #repl{}.
+
+-record(state, {mod :: gen_tcp | ssl | undefined,
+                sock :: tcp_socket() | ssl:sslsocket() | undefined,
                 data = <<>>,
-                backend,
-                handler,
-                codec,
-                queue = queue:new(),
-                async,
-                parameters = [],
-                types = [],
-                columns = [],
-                rows = [],
+                backend :: {Pid :: integer(), Key :: integer()} | undefined,
+                handler = on_message :: on_message | on_replication | undefined,
+                codec :: epgsql_binary:codec() | undefined,
+                queue = queue:new() :: queue:queue({epgsql_command:command(), any(), transport()}),
+                current_cmd :: epgsql_command:command() | undefined,
+                current_cmd_state :: any() | undefined,
+                current_cmd_transport :: transport() | undefined,
+                async :: undefined | atom() | pid(),
+                parameters = [] :: [{Key :: binary(), Value :: binary()}],
+                rows = [] :: [tuple()],
                 results = [],
-                batch = [],
-                sync_required,
-                txstatus,
-                complete_status :: undefined | atom() | {atom(), integer()},
-                repl_last_received_lsn,
-                repl_last_flushed_lsn,
-                repl_last_applied_lsn,
-                repl_feedback_required,
-                repl_cbmodule,
-                repl_cbstate,
-                repl_receiver,
-                repl_align_lsn}).
+                sync_required :: boolean() | undefined,
+                txstatus :: byte() | undefined,  % $I | $T | $E,
+                complete_status :: atom() | {atom(), integer()} | undefined,
+                repl :: repl_state() | undefined}).
+
+-opaque pg_sock() :: #state{}.
 
 %% -- client interface --
 
@@ -101,6 +89,18 @@ close(C) when is_pid(C) ->
     catch gen_server:cast(C, stop),
     ok.
 
+-spec sync_command(epgsql:connection(), epgsql_command:command(), any()) -> any().
+sync_command(C, Command, Args) ->
+    gen_server:call(C, {command, Command, Args}, infinity).
+
+-spec async_command(epgsql:connection(), cast | incremental,
+                    epgsql_command:command(), any()) -> reference().
+async_command(C, Transport, Command, Args) ->
+    Ref = make_ref(),
+    Pid = self(),
+    ok = gen_server:cast(C, {{Transport, Pid, Ref}, Command, Args}),
+    Ref.
+
 get_parameter(C, Name) ->
     gen_server:call(C, {get_parameter, to_binary(Name)}, infinity).
 
@@ -114,21 +114,72 @@ get_cmd_status(C) ->
 cancel(S) ->
     gen_server:cast(S, cancel).
 
+
+%% -- command APIs --
+
+%% send()
+%% send_many()
+
+-spec set_net_socket(gen_tcp | ssl, tcp_socket() | ssl:sslsocket(), pg_sock()) -> pg_sock().
+set_net_socket(Mod, Socket, State) ->
+    State1 = State#state{mod = Mod, sock = Socket},
+    setopts(State1, [{active, true}]),
+    State1.
+
+-spec init_replication_state(pg_sock()) -> pg_sock().
+init_replication_state(State) ->
+    State#state{repl = #repl{}}.
+
+-spec set_attr(atom(), any(), pg_sock()) -> pg_sock().
+set_attr(backend, {_Pid, _Key} = Backend, State) ->
+    State#state{backend = Backend};
+set_attr(async, Async, State) ->
+    State#state{async = Async};
+set_attr(txstatus, Status, State) ->
+    State#state{txstatus = Status};
+set_attr(codec, Codec, State) ->
+    State#state{codec = Codec};
+set_attr(sync_required, Value, State) ->
+    State#state{sync_required = Value};
+set_attr(replication_state, Value, State) ->
+    State#state{repl = Value}.
+
+%% XXX: be careful!
+-spec set_packet_handler(atom(), pg_sock()) -> pg_sock().
+set_packet_handler(Handler, State) ->
+    State#state{handler = Handler}.
+
+-spec get_codec(pg_sock()) -> epgsql_binary:codec().
+get_codec(#state{codec = Codec}) ->
+    Codec.
+
+-spec get_replication_state(pg_sock()) -> repl_state().
+get_replication_state(#state{repl = Repl}) ->
+    Repl.
+
+-spec get_rows(pg_sock()) -> [tuple()].
+get_rows(#state{rows = Rows}) ->
+    lists:reverse(Rows).
+
+-spec get_results(pg_sock()) -> [any()].
+get_results(#state{results = Results}) ->
+    lists:reverse(Results).
+
+-spec get_parameter_internal(binary(), pg_sock()) -> binary() | undefined.
+get_parameter_internal(Name, #state{parameters = Parameters}) ->
+    case lists:keysearch(Name, 1, Parameters) of
+        {value, {Name, Value}} -> Value;
+        false                  -> undefined
+    end.
+
+
 %% -- gen_server implementation --
 
 init([]) ->
     {ok, #state{}}.
 
-handle_call({update_type_cache, TypeInfos}, _From, #state{codec = Codec} = State) ->
-    Codec2 = epgsql_binary:update_type_cache(TypeInfos, Codec),
-    {reply, ok, State#state{codec = Codec2}};
-
 handle_call({get_parameter, Name}, _From, State) ->
-    Value1 = case lists:keysearch(Name, 1, State#state.parameters) of
-        {value, {Name, Value}} -> Value;
-        false                  -> undefined
-    end,
-    {reply, {ok, Value1}, State};
+    {reply, {ok, get_parameter_internal(Name, State)}, State};
 
 handle_call({set_async_receiver, PidOrName}, _From, #state{async = Previous} = State) ->
     {reply, {ok, Previous}, State#state{async = PidOrName}};
@@ -137,23 +188,21 @@ handle_call(get_cmd_status, _From, #state{complete_status = Status} = State) ->
     {reply, {ok, Status}, State};
 
 handle_call({standby_status_update, FlushedLSN, AppliedLSN}, _From,
-    #state{repl_last_received_lsn = ReceivedLSN} = State) ->
+            #state{handler = on_replication,
+                   repl = #repl{last_received_lsn = ReceivedLSN} = Repl} = State) ->
     send(State, ?COPY_DATA, epgsql_wire:encode_standby_status_update(ReceivedLSN, FlushedLSN, AppliedLSN)),
-    {reply, ok, State#state{repl_last_flushed_lsn = FlushedLSN, repl_last_applied_lsn = AppliedLSN}};
-
-handle_call(Command, From, State) ->
-    #state{queue = Q} = State,
-    Req = {{call, From}, Command},
-    command(Command, State#state{queue = queue:in(Req, Q),
-                                 complete_status = undefined}).
-
-handle_cast({{Method, From, Ref}, Command} = Req, State)
+    Repl1 = Repl#repl{last_flushed_lsn = FlushedLSN,
+                      last_applied_lsn = AppliedLSN},
+    {reply, ok, State#state{repl = Repl1}};
+handle_call({command, Command, Args}, From, State) ->
+    Transport = {call, From},
+    command_new(Transport, Command, Args, State).
+
+handle_cast({{Method, From, Ref} = Transport, Command, Args}, State)
   when ((Method == cast) or (Method == incremental)),
        is_pid(From),
        is_reference(Ref)  ->
-    #state{queue = Q} = State,
-    command(Command, State#state{queue = queue:in(Req, Q),
-                                 complete_status = undefined});
+    command_new(Transport, Command, Args, State);
 
 handle_cast(stop, State) ->
     {stop, normal, flush_queue(State, {error, closed})};
@@ -199,236 +248,134 @@ code_change(_OldVsn, State, _Extra) ->
 
 %% -- internal functions --
 
-command(Command, State = #state{sync_required = true})
-  when Command /= sync ->
-    {noreply, finish(State, {error, sync_required})};
-
-command({connect, Host, Username, Password, Opts}, State) ->
-    Timeout = proplists:get_value(timeout, Opts, 5000),
-    Port = proplists:get_value(port, Opts, 5432),
-    SockOpts = [{active, false}, {packet, raw}, binary, {nodelay, true}, {keepalive, true}],
-    case gen_tcp:connect(Host, Port, SockOpts, Timeout) of
-        {ok, Sock} ->
-
-            %% Increase the buffer size.  Following the recommendation in the inet man page:
-            %%
-            %%    It is recommended to have val(buffer) >=
-            %%    max(val(sndbuf),val(recbuf)).
-
-            {ok, [{recbuf, RecBufSize}, {sndbuf, SndBufSize}]} =
-                inet:getopts(Sock, [recbuf, sndbuf]),
-            inet:setopts(Sock, [{buffer, max(RecBufSize, SndBufSize)}]),
-
-            State2 = case proplists:get_value(ssl, Opts) of
-                         T when T == true; T == required ->
-                             start_ssl(Sock, T, Opts, State);
-                         _ ->
-                             State#state{mod  = gen_tcp, sock = Sock}
-                     end,
-
-            Opts2 = ["user", 0, Username, 0],
-            Opts3 = case proplists:get_value(database, Opts, undefined) of
-                undefined -> Opts2;
-                Database  -> [Opts2 | ["database", 0, Database, 0]]
-            end,
-
-            Opts4 = case proplists:get_value(replication, Opts, undefined) of
-                        undefined -> Opts3;
-                        Replication  -> [Opts3 | ["replication", 0, Replication, 0]]
-                    end,
+-spec command_new(transport(), epgsql_command:command(), any(), pg_sock()) ->
+                         Result when
+      Result :: {noreply, pg_sock()}
+              | {stop, Reason :: any(), pg_sock()}.
+command_new(Transport, Command, Args, State) ->
+    CmdState = epgsql_command:init(Command, Args),
+    command_exec(Transport, Command, CmdState, State).
+
+-spec command_exec(transport(), epgsql_command:command(), any(), pg_sock()) ->
+                          Result when
+      Result :: {noreply, pg_sock()}
+              | {stop, Reason :: any(), pg_sock()}.
+command_exec(Transport, Command, _, State = #state{sync_required = true})
+  when Command /= epgsql_cmd_sync ->
+    {noreply,
+     finish(State#state{current_cmd = Command,
+                        current_cmd_transport = Transport},
+            {error, sync_required})};
+command_exec(Transport, Command, CmdState, State) ->
+    case epgsql_command:execute(Command, State, CmdState) of
+        {ok, State1, CmdState1} ->
+            {noreply, command_enqueue(Transport, Command, CmdState1, State1)};
+        {stop, StopReason, Response, State1} ->
+            reply(Transport, Response, Response),
+            {stop, StopReason, State1}
+    end.
 
-            send(State2, [<<196608:?int32>>, Opts4, 0]),
-            Async   = proplists:get_value(async, Opts, undefined),
-            setopts(State2, [{active, true}]),
-            put(username, Username),
-            put(password, Password),
+-spec command_enqueue(transport(), epgsql_command:command(), epgsql_command:state(), pg_sock()) -> pg_sock().
+command_enqueue(Transport, Command, CmdState, #state{current_cmd = undefined} = State) ->
+    State#state{current_cmd = Command,
+                current_cmd_state = CmdState,
+                current_cmd_transport = Transport,
+                complete_status = undefined};
+command_enqueue(Transport, Command, CmdState, #state{queue = Q} = State) ->
+    State#state{queue = queue:in({Command, CmdState, Transport}, Q),
+                complete_status = undefined}.
+
+-spec command_handle_message(byte(), binary() | epgsql:query_error(), pg_sock()) ->
+                                    {noreply, pg_sock()}
+                                  | {stop, any(), pg_sock()}.
+command_handle_message(Msg, Payload,
+                       #state{current_cmd = Command,
+                              current_cmd_state = CmdState} = State) ->
+    case epgsql_command:handle_message(Command, Msg, Payload, State, CmdState) of
+        {add_row, Row, State1, CmdState1} ->
+            {noreply, add_row(State1#state{current_cmd_state = CmdState1}, Row)};
+        {add_result, Result, Notice, State1, CmdState1} ->
             {noreply,
-             State2#state{handler = auth,
-                          async = Async}};
-
-        {error, Reason} = Error ->
-            {stop, Reason, finish(State, Error)}
-    end;
-
-command({squery, Sql}, State) ->
-    send(State, ?SIMPLEQUERY, [Sql, 0]),
-    {noreply, State};
-
-%% TODO add fast_equery command that doesn't need parsed statement,
-%% uses default (text) column format,
-%% sends Describe after Bind to get RowDescription
-command({equery, Statement, Parameters}, #state{codec = Codec} = State) ->
-    #statement{name = StatementName, columns = Columns} = Statement,
-    Bin1 = epgsql_wire:encode_parameters(Parameters, Codec),
-    Bin2 = epgsql_wire:encode_formats(Columns),
-    send_multi(State, [
-        {?BIND, ["", 0, StatementName, 0, Bin1, Bin2]},
-        {?EXECUTE, ["", 0, <<0:?int32>>]},
-        {?CLOSE, [?PREPARED_STATEMENT, StatementName, 0]},
-        {?SYNC, []}
-    ]),
-    {noreply, State};
-
-command({prepared_query, Statement, Parameters}, #state{codec = Codec} = State) ->
-    #statement{name = StatementName, columns = Columns} = Statement,
-    Bin1 = epgsql_wire:encode_parameters(Parameters, Codec),
-    Bin2 = epgsql_wire:encode_formats(Columns),
-    send_multi(State, [
-        {?BIND, ["", 0, StatementName, 0, Bin1, Bin2]},
-        {?EXECUTE, ["", 0, <<0:?int32>>]},
-        {?SYNC, []}
-    ]),
-    {noreply, State};
-
-command({parse, Name, Sql, Types}, State) ->
-    Bin = epgsql_wire:encode_types(Types, State#state.codec),
-    send_multi(State, [
-        {?PARSE, [Name, 0, Sql, 0, Bin]},
-        {?DESCRIBE, [?PREPARED_STATEMENT, Name, 0]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command({bind, Statement, PortalName, Parameters}, #state{codec = Codec} = State) ->
-    #statement{name = StatementName, columns = Columns, types = Types} = Statement,
-    Typed_Parameters = lists:zip(Types, Parameters),
-    Bin1 = epgsql_wire:encode_parameters(Typed_Parameters, Codec),
-    Bin2 = epgsql_wire:encode_formats(Columns),
-    send_multi(State, [
-        {?BIND, [PortalName, 0, StatementName, 0, Bin1, Bin2]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command({execute, _Statement, PortalName, MaxRows}, State) ->
-    send_multi(State, [
-        {?EXECUTE, [PortalName, 0, <<MaxRows:?int32>>]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command({execute_batch, Batch}, #state{codec = Codec} = State) ->
-    Commands =
-        lists:foldr(
-          fun({Statement, Parameters}, Acc) ->
-                  #statement{name = StatementName,
-                             columns = Columns,
-                             types = Types} = Statement,
-                  Typed_Parameters = lists:zip(Types, Parameters),
-                  Bin1 = epgsql_wire:encode_parameters(Typed_Parameters, Codec),
-                  Bin2 = epgsql_wire:encode_formats(Columns),
-                  [{?BIND, [0, StatementName, 0, Bin1, Bin2]},
-                   {?EXECUTE, [0, <<0:?int32>>]} | Acc]
-          end,
-          [{?SYNC, []}],
-          Batch),
-    send_multi(State, Commands),
-    {noreply, State};
-
-command({describe_statement, Name}, State) ->
-    send_multi(State, [
-        {?DESCRIBE, [?PREPARED_STATEMENT, Name, 0]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command({describe_portal, Name}, State) ->
-    send_multi(State, [
-        {?DESCRIBE, [?PORTAL, Name, 0]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command({close, Type, Name}, State) ->
-    Type2 = case Type of
-        statement -> ?PREPARED_STATEMENT;
-        portal    -> ?PORTAL
-    end,
-    send_multi(State, [
-        {?CLOSE, [Type2, Name, 0]},
-        {?FLUSH, []}
-    ]),
-    {noreply, State};
-
-command(sync, State) ->
-    send(State, ?SYNC, []),
-    {noreply, State#state{sync_required = false}};
-
-command({start_replication, ReplicationSlot, Callback, CbInitState, WALPosition, PluginOpts, Opts}, State) ->
-    Sql1 = ["START_REPLICATION SLOT """, ReplicationSlot, """ LOGICAL ", WALPosition],
-    Sql2 =
-        case PluginOpts of
-            [] -> Sql1;
-            PluginOpts -> [Sql1 , " (", PluginOpts, ")"]
-        end,
-
-    State2 =
-        case Callback of
-            Pid when is_pid(Pid) -> State#state{repl_receiver = Pid};
-            Module -> State#state{repl_cbmodule = Module, repl_cbstate = CbInitState}
-        end,
+             add_result(State1#state{current_cmd_state = CmdState1},
+                        Notice, Result)};
+        {finish, Result, Notice, State1} ->
+            {noreply, finish(State1, Notice, Result)};
+        {noaction, State1} ->
+            {noreply, State1};
+        {noaction, State1, CmdState1} ->
+            {noreply, State1#state{current_cmd_state = CmdState1}};
+        {requeue, State1, CmdState1} ->
+            Transport = State1#state.current_cmd_transport,
+            command_exec(Transport, Command, CmdState1,
+                         State1#state{current_cmd = undefined});
+        {stop, Reason, Response, State1} ->
+            {stop, Reason, finish(State1, Response)};
+        {sync_required, Why} ->
+            %% Protocol error. Finish and flush all pending commands.
+            {noreply, sync_required(finish(State#state{sync_required = true}, Why))};
+        unknown ->
+            {stop, {error, {unexpected_message, Msg, Command, CmdState}}, State}
+    end.
 
-    Hex = [H || H <- WALPosition, H =/= $/],
-    {ok, [LSN], _} = io_lib:fread("~16u", Hex),
-    AlignLsn = proplists:get_value(align_lsn, Opts, false),
-    State3 = State2#state{repl_last_flushed_lsn = LSN, repl_last_applied_lsn = LSN, repl_align_lsn = AlignLsn},
-
-    send(State3, ?SIMPLEQUERY, [Sql2, 0]),
-    {noreply, State3}.
-
-start_ssl(S, Flag, Opts, State) ->
-    ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>),
-    Timeout = proplists:get_value(timeout, Opts, 5000),
-    {ok, <<Code>>} = gen_tcp:recv(S, 1, Timeout),
-    case Code of
-        $S  ->
-            SslOpts = proplists:get_value(ssl_opts, Opts, []),
-            case ssl:connect(S, SslOpts, Timeout) of
-                {ok, S2}        -> State#state{mod = ssl, sock = S2};
-                {error, Reason} -> exit({ssl_negotiation_failed, Reason})
-            end;
-        $N ->
-            case Flag of
-                true     -> State;
-                required -> exit(ssl_not_available)
-            end
+command_next(#state{current_cmd = PrevCmd,
+                    queue = Q} = State) when PrevCmd =/= undefined ->
+    case queue:out(Q) of
+        {empty, _} ->
+            State#state{current_cmd = undefined,
+                        current_cmd_state = undefined,
+                        current_cmd_transport = undefined,
+                        rows = [],
+                        results = []};
+        {{value, {Command, CmdState, Transport}}, Q1} ->
+            State#state{current_cmd = Command,
+                        current_cmd_state = CmdState,
+                        current_cmd_transport = Transport,
+                        queue = Q1,
+                        rows = [],
+                        results = []}
     end.
 
+
 setopts(#state{mod = Mod, sock = Sock}, Opts) ->
     case Mod of
         gen_tcp -> inet:setopts(Sock, Opts);
         ssl     -> ssl:setopts(Sock, Opts)
     end.
 
+%% This one only used in connection initiation to send client's
+%% `StartupMessage' and `SSLRequest' packets
+-spec send(pg_sock(), iodata()) -> ok | {error, any()}.
 send(#state{mod = Mod, sock = Sock}, Data) ->
-    do_send(Mod, Sock, epgsql_wire:encode(Data)).
+    do_send(Mod, Sock, epgsql_wire:encode_command(Data)).
 
+-spec send(pg_sock(), byte(), iodata()) -> ok | {error, any()}.
 send(#state{mod = Mod, sock = Sock}, Type, Data) ->
-    do_send(Mod, Sock, epgsql_wire:encode(Type, Data)).
+    do_send(Mod, Sock, epgsql_wire:encode_command(Type, Data)).
 
+-spec send_multi(pg_sock(), [{byte(), iodata()}]) -> ok | {error, any()}.
 send_multi(#state{mod = Mod, sock = Sock}, List) ->
     do_send(Mod, Sock, lists:map(fun({Type, Data}) ->
-        epgsql_wire:encode(Type, Data)
+        epgsql_wire:encode_command(Type, Data)
     end, List)).
 
 do_send(gen_tcp, Sock, Bin) ->
+    %% Why not gen_tcp:send/2?
+    %% See https://github.com/rabbitmq/rabbitmq-common/blob/v3.7.4/src/rabbit_writer.erl#L367-L384
+    %% Because of that we also have `handle_info({inet_reply, ...`
     try erlang:port_command(Sock, Bin) of
         true ->
             ok
     catch
         error:_Error ->
-            {error,einval}
+            {error, einval}
     end;
+do_send(ssl, Sock, Bin) ->
+    ssl:send(Sock, Bin).
 
-do_send(Mod, Sock, Bin) ->
-    Mod:send(Sock, Bin).
-
-loop(#state{data = Data, handler = Handler, repl_last_received_lsn = LastReceivedLSN,
-    repl_last_flushed_lsn = LastFlushedLSN, repl_last_applied_lsn = LastAppliedLSN,
-    repl_feedback_required = ReplFeedbackRequired} = State) ->
+loop(#state{data = Data, handler = Handler, repl = Repl} = State) ->
     case epgsql_wire:decode_message(Data) of
-        {Message, Tail} ->
-            case ?MODULE:Handler(Message, State#state{data = Tail}) of
+        {Type, Payload, Tail} ->
+            case ?MODULE:Handler(Type, Payload, State#state{data = Tail}) of
                 {noreply, State2} ->
                     loop(State2);
                 R = {stop, _Reason2, _State2} ->
@@ -436,11 +383,14 @@ loop(#state{data = Data, handler = Handler, repl_last_received_lsn = LastReceive
             end;
         _ ->
             %% in replication mode send feedback after each batch of messages
-            case ReplFeedbackRequired of
+            case (Repl =/= undefined) andalso (Repl#repl.feedback_required) of
                 true ->
+                    #repl{last_received_lsn = LastReceivedLSN,
+                          last_flushed_lsn = LastFlushedLSN,
+                          last_applied_lsn = LastAppliedLSN} = Repl,
                     send(State, ?COPY_DATA, epgsql_wire:encode_standby_status_update(
                         LastReceivedLSN, LastFlushedLSN, LastAppliedLSN)),
-                    {noreply, State#state{repl_feedback_required = false}};
+                    {noreply, State#state{repl = Repl#repl{feedback_required = false}}};
                 _ ->
                     {noreply, State}
             end
@@ -449,44 +399,31 @@ loop(#state{data = Data, handler = Handler, repl_last_received_lsn = LastReceive
 finish(State, Result) ->
     finish(State, Result, Result).
 
-finish(State = #state{queue = Q}, Notice, Result) ->
-    case queue:get(Q) of
-        {{cast, From, Ref}, _} ->
-            From ! {self(), Ref, Result};
-        {{incremental, From, Ref}, _} ->
-            From ! {self(), Ref, Notice};
-        {{call, From}, _} ->
-            gen_server:reply(From, Result)
-    end,
-    State#state{queue = queue:drop(Q),
-                types = [],
-                columns = [],
-                rows = [],
-                results = [],
-                batch = []}.
+finish(State = #state{current_cmd_transport = Transport}, Notice, Result) ->
+    reply(Transport, Notice, Result),
+    command_next(State).
 
-add_result(State, Notice, Result) ->
-    #state{queue = Q, results = Results, batch = Batch} = State,
-    Results2 = case queue:get(Q) of
-                   {{incremental, From, Ref}, _} ->
+reply({cast, From, Ref}, _, Result) ->
+    From ! {self(), Ref, Result};
+reply({incremental, From, Ref}, Notice, _) ->
+    From ! {self(), Ref, Notice};
+reply({call, From}, _, Result) ->
+    gen_server:reply(From, Result).
+
+add_result(#state{results = Results, current_cmd_transport = Transport} = State, Notice, Result) ->
+    Results2 = case Transport of
+                   {incremental, From, Ref} ->
                        From ! {self(), Ref, Notice},
                        Results;
                    _ ->
                        [Result | Results]
                end,
-    Batch2 = case Batch of
-                 [] -> [];
-                 _ -> tl(Batch)
-             end,
-    State#state{types = [],
-                columns = [],
-                rows = [],
-                results = Results2,
-                batch = Batch2}.
-
-add_row(State = #state{queue = Q, rows = Rows}, Data) ->
-    Rows2 = case queue:get(Q) of
-                {{incremental, From, Ref}, _} ->
+    State#state{rows = [],
+                results = Results2}.
+
+add_row(#state{rows = Rows, current_cmd_transport = Transport} = State, Data) ->
+    Rows2 = case Transport of
+                {incremental, From, Ref} ->
                     From ! {self(), Ref, {data, Data}},
                     Rows;
                 _ ->
@@ -494,15 +431,13 @@ add_row(State = #state{queue = Q, rows = Rows}, Data) ->
             end,
     State#state{rows = Rows2}.
 
-notify(State = #state{queue = Q}, Notice) ->
-    case queue:get(Q) of
-        {{incremental, From, Ref}, _} ->
-            From ! {self(), Ref, Notice};
-        _ ->
-            ignore
-    end,
+notify(#state{current_cmd_transport = {incremental, From, Ref}} = State, Notice) ->
+    From ! {self(), Ref, Notice},
+    State;
+notify(State, _) ->
     State.
 
+%% Send asynchronous messages (notice / notification)
 notify_async(#state{async = undefined}, _) ->
     false;
 notify_async(#state{async = PidOrName}, Msg) ->
@@ -513,311 +448,58 @@ notify_async(#state{async = PidOrName}, Msg) ->
             false
     end.
 
-command_tag(#state{queue = Q}) ->
-    {_, Req} = queue:get(Q),
-    if is_tuple(Req) ->
-            element(1, Req);
-       is_atom(Req) ->
-            Req
-    end.
-
-get_columns(State) ->
-    #state{queue = Q, columns = Columns, batch = Batch} = State,
-    case queue:get(Q) of
-        {_, {Command, #statement{columns = C}, _}}  when Command == equery; Command == prepared_query ->
-            C;
-        {_, {execute, #statement{columns = C}, _, _}} ->
-            C;
-        {_, {squery, _}} ->
-            Columns;
-        {_, {execute_batch, _}} ->
-            [{#statement{columns = C}, _} | _] = Batch,
-            C
-    end.
-
-make_statement(State) ->
-    #state{queue = Q, types = Types, columns = Columns} = State,
-    Name = case queue:get(Q) of
-               {_, {parse, N, _, _}} -> N;
-               {_, {describe_statement, N}} -> N
-           end,
-    #statement{name = Name, types = Types, columns = Columns}.
-
-sync_required(#state{queue = Q} = State) ->
-    case queue:is_empty(Q) of
-        false ->
-            case command_tag(State) of
-                sync ->
-                    State;
-                _ ->
-                    sync_required(finish(State, {error, sync_required}))
-            end;
-        true ->
-            State#state{sync_required = true}
-    end.
+sync_required(#state{current_cmd = epgsql_cmd_sync} = State) ->
+    State;
+sync_required(#state{current_cmd = undefined} = State) ->
+    State#state{sync_required = true};
+sync_required(State) ->
+    sync_required(finish(State, {error, sync_required})).
 
-flush_queue(#state{queue = Q} = State, Error) ->
-    case queue:is_empty(Q) of
-        false ->
-            flush_queue(finish(State, Error), Error);
-        true -> State
-    end.
+flush_queue(#state{current_cmd = undefined} = State, _) ->
+    State;
+flush_queue(State, Error) ->
+    flush_queue(finish(State, Error), Error).
 
 to_binary(B) when is_binary(B) -> B;
 to_binary(L) when is_list(L)   -> list_to_binary(L).
 
-hex(Bin) ->
-    HChar = fun(N) when N < 10 -> $0 + N;
-               (N) when N < 16 -> $W + N
-            end,
-    <<<<(HChar(H)), (HChar(L))>> || <<H:4, L:4>> <= Bin>>.
 
 %% -- backend message handling --
 
-%% AuthenticationOk
-auth({?AUTHENTICATION_REQUEST, <<0:?int32>>}, State) ->
-    {noreply, State#state{handler = initializing}};
-
-%% AuthenticationCleartextPassword
-auth({?AUTHENTICATION_REQUEST, <<3:?int32>>}, State) ->
-    send(State, ?PASSWORD, [get(password), 0]),
-    {noreply, State};
-
-%% AuthenticationMD5Password
-auth({?AUTHENTICATION_REQUEST, <<5:?int32, Salt:4/binary>>}, State) ->
-    Digest1 = hex(erlang:md5([get(password), get(username)])),
-    Str = ["md5", hex(erlang:md5([Digest1, Salt])), 0],
-    send(State, ?PASSWORD, Str),
-    {noreply, State};
-
-auth({?AUTHENTICATION_REQUEST, <<M:?int32, _/binary>>}, State) ->
-    Method = case M of
-        2 -> kerberosV5;
-        4 -> crypt;
-        6 -> scm;
-        7 -> gss;
-        8 -> sspi;
-        _ -> unknown
-    end,
-    State2 = finish(State, {error, {unsupported_auth_method, Method}}),
-    {stop, normal, State2};
-
-%% ErrorResponse
-auth({error, E}, State) ->
-    Why = case E#error.code of
-        <<"28000">> -> invalid_authorization_specification;
-        <<"28P01">> -> invalid_password;
-        Any         -> Any
-    end,
-    {stop, normal, finish(State, {error, Why})};
-
-auth(Other, State) ->
-    on_message(Other, State).
-
-%% BackendKeyData
-initializing({?CANCELLATION_KEY, <<Pid:?int32, Key:?int32>>}, State) ->
-    {noreply, State#state{backend = {Pid, Key}}};
-
-%% ReadyForQuery
-initializing({?READY_FOR_QUERY, <<Status:8>>}, State) ->
-    #state{parameters = Parameters} = State,
-    erase(username),
-    erase(password),
-    %% TODO decode dates to now() format
-    case lists:keysearch(<<"integer_datetimes">>, 1, Parameters) of
-        {value, {_, <<"on">>}}  -> put(datetime_mod, epgsql_idatetime);
-        {value, {_, <<"off">>}} -> put(datetime_mod, epgsql_fdatetime)
-    end,
-    State2 = finish(State#state{handler = on_message,
-                               txstatus = Status,
-                               codec = epgsql_binary:new_codec([])},
-                   connected),
-    {noreply, State2};
-
-initializing({error, _} = Error, State) ->
-    {stop, normal, finish(State, Error)};
-
-initializing(Other, State) ->
-    on_message(Other, State).
-
-%% ParseComplete
-on_message({?PARSE_COMPLETE, <<>>}, State) ->
-    {noreply, State};
-
-%% ParameterDescription
-on_message({?PARAMETER_DESCRIPTION, <<_Count:?int16, Bin/binary>>}, State) ->
-    Types = [epgsql_binary:oid2type(Oid, State#state.codec) || <<Oid:?int32>> <= Bin],
-    State2 = notify(State#state{types = Types}, {types, Types}),
-    {noreply, State2};
-
-%% RowDescription
-on_message({?ROW_DESCRIPTION, <<Count:?int16, Bin/binary>>}, State) ->
-    Columns = epgsql_wire:decode_columns(Count, Bin, State#state.codec),
-    Columns2 =
-        case command_tag(State) of
-            C when C == describe_portal; C == squery ->
-                Columns;
-            C when C == parse; C == describe_statement ->
-                [Col#column{format = epgsql_wire:format(Col#column.type)}
-                 || Col <- Columns]
-        end,
-    State2 = State#state{columns = Columns2},
-    Message = {columns, Columns2},
-    State3 = case command_tag(State2) of
-                 squery ->
-                     notify(State2, Message);
-                 T when T == parse; T == describe_statement ->
-                     finish(State2, Message, {ok, make_statement(State2)});
-                 describe_portal ->
-                     finish(State2, Message, {ok, Columns})
-             end,
-    {noreply, State3};
-
-%% NoData
-on_message({?NO_DATA, <<>>}, State) ->
-    State2 = case command_tag(State) of
-                 C when C == parse; C == describe_statement ->
-                     finish(State, no_data, {ok, make_statement(State)});
-                 describe_portal ->
-                     finish(State, no_data, {ok, []})
-             end,
-    {noreply, State2};
-
-%% BindComplete
-on_message({?BIND_COMPLETE, <<>>}, State) ->
-    State2 = case command_tag(State) of
-                 Command when Command == equery; Command == prepared_query ->
-                     %% TODO send Describe as a part of equery, needs text format support
-                     notify(State, {columns, get_columns(State)});
-                 bind ->
-                     finish(State, ok);
-                 execute_batch ->
-                     Batch =
-                         case State#state.batch of
-                             [] ->
-                                 {_, {_, B}} = queue:get(State#state.queue),
-                                 B;
-                             B -> B
-                         end,
-                     State#state{batch = Batch}
-             end,
-    {noreply, State2};
-
-%% CloseComplete
-on_message({?CLOSE_COMPLETE, <<>>}, State) ->
-    State2 = case command_tag(State) of
-                 Command when Command == equery; Command == prepared_query ->
-                     State;
-                 close ->
-                     finish(State, ok)
-             end,
-    {noreply, State2};
-
-%% DataRow
-on_message({?DATA_ROW, <<_Count:?int16, Bin/binary>>}, State) ->
-    Data = epgsql_wire:decode_data(get_columns(State), Bin, State#state.codec),
-    {noreply, add_row(State, Data)};
-
-%% PortalSuspended
-on_message({?PORTAL_SUSPENDED, <<>>}, State) ->
-    State2 = finish(State,
-                   suspended,
-                   {partial, lists:reverse(State#state.rows)}),
-    {noreply, State2};
-
 %% CommandComplete
-on_message({?COMMAND_COMPLETE, Bin}, State0) ->
+on_message(?COMMAND_COMPLETE = Msg, Bin, State) ->
     Complete = epgsql_wire:decode_complete(Bin),
-    State = State0#state{complete_status = Complete},
-    Command = command_tag(State),
-    Notice = {complete, Complete},
-    Rows = lists:reverse(State#state.rows),
-    Columns = get_columns(State),
-    State2 = case {Command, Complete, Columns} of
-                 {execute, {_, Count}, []} ->
-                     finish(State, Notice, {ok, Count});
-                 {execute, {_, Count}, _} ->
-                     finish(State, Notice, {ok, Count, Rows});
-                 {execute, _, _} ->
-                     finish(State, Notice, {ok, Rows});
-                 {execute_batch, {_, Count}, []} ->
-                     add_result(State, Notice, {ok, Count});
-                 {execute_batch, {_, Count}, _} ->
-                     add_result(State, Notice, {ok, Count, Rows});
-                 {execute_batch, _, _} ->
-                     add_result(State, Notice, {ok, Rows});
-                 {C, {_, Count}, []} when C == squery; C == equery; C == prepared_query ->
-                     add_result(State, Notice, {ok, Count});
-                 {C, {_, Count}, _} when C == squery; C == equery; C == prepared_query ->
-                     add_result(State, Notice, {ok, Count, Columns, Rows});
-                 {C, _, _} when C == squery; C == equery; C == prepared_query ->
-                     add_result(State, Notice, {ok, Columns, Rows})
-             end,
-    {noreply, State2};
-
-%% EmptyQueryResponse
-on_message({?EMPTY_QUERY, _Bin}, State) ->
-    Notice = {complete, empty},
-    State2 = case command_tag(State) of
-                 execute ->
-                     finish(State, Notice, {ok, [], []});
-                 C when C == squery; C == equery; C == prepared_query ->
-                     add_result(State, Notice, {ok, [], []})
-             end,
-    {noreply, State2};
+    command_handle_message(Msg, Bin, State#state{complete_status = Complete});
 
 %% ReadyForQuery
-on_message({?READY_FOR_QUERY, <<Status:8>>}, State) ->
-    State2 = case command_tag(State) of
-                 squery ->
-                     case State#state.results of
-                         [Result] ->
-                             finish(State, done, Result);
-                         Results ->
-                             finish(State, done, lists:reverse(Results))
-                     end;
-                 execute_batch ->
-                     finish(State, done, lists:reverse(State#state.results));
-                 Command when Command == equery; Command == prepared_query ->
-                     case State#state.results of
-                         [Result] ->
-                             finish(State, done, Result);
-                         [] ->
-                             finish(State, done)
-                     end;
-                 sync ->
-                     finish(State, ok)
-             end,
-    {noreply, State2#state{txstatus = Status}};
-
-on_message(Error = {error, Reason}, State) ->
-    case queue:is_empty(State#state.queue) of
-        true ->
+on_message(?READY_FOR_QUERY = Msg, <<Status:8>> = Bin, State) ->
+    command_handle_message(Msg, Bin, State#state{txstatus = Status});
+
+%% Error
+on_message(?ERROR = Msg, Err, #state{current_cmd = CurrentCmd} = State) ->
+    Reason = epgsql_wire:decode_error(Err),
+    case CurrentCmd of
+        undefined ->
+            %% Message generated by server asynchronously
             {stop, {shutdown, Reason}, State};
-        false ->
-            State2 = case command_tag(State) of
-                C when C == squery; C == equery; C == execute_batch; C == prepared_query ->
-                    add_result(State, Error, Error);
-                _ ->
-                    sync_required(finish(State, Error))
-            end,
-            {noreply, State2}
+        _ ->
+            command_handle_message(Msg, Reason, State)
     end;
 
 %% NoticeResponse
-on_message({?NOTICE, Data}, State) ->
+on_message(?NOTICE, Data, State) ->
     notify_async(State, {notice, epgsql_wire:decode_error(Data)}),
     {noreply, State};
 
 %% ParameterStatus
-on_message({?PARAMETER_STATUS, Data}, State) ->
+on_message(?PARAMETER_STATUS, Data, State) ->
     [Name, Value] = epgsql_wire:decode_strings(Data),
     Parameters2 = lists:keystore(Name, 1, State#state.parameters,
                                  {Name, Value}),
     {noreply, State#state{parameters = Parameters2}};
 
 %% NotificationResponse
-on_message({?NOTIFICATION, <<Pid:?int32, Strings/binary>>}, State) ->
+on_message(?NOTIFICATION, <<Pid:?int32, Strings/binary>>, State) ->
     {Channel1, Payload1} = case epgsql_wire:decode_strings(Strings) of
         [Channel, Payload] -> {Channel, Payload};
         [Channel]          -> {Channel, <<>>}
@@ -825,46 +507,72 @@ on_message({?NOTIFICATION, <<Pid:?int32, Strings/binary>>}, State) ->
     notify_async(State, {notification, Channel1, Pid, Payload1}),
     {noreply, State};
 
+%% ParseComplete
+%% ParameterDescription
+%% RowDescription
+%% NoData
+%% BindComplete
+%% CloseComplete
+%% DataRow
+%% PortalSuspended
+%% EmptyQueryResponse
+%% CopyData
 %% CopyBothResponse
-on_message({?COPY_BOTH_RESPONSE, _Data}, State) ->
-    State2 = finish(State, ok),
-    {noreply, State2};
+on_message(Msg, Payload, State) ->
+    command_handle_message(Msg, Payload, State).
 
-%% CopyData for COPY command. COPY command not supported yet.
-on_message({?COPY_DATA, _Data}, #state{repl_cbmodule = undefined, repl_receiver = undefined} = State) ->
-    {stop, {error, copy_command_not_supported}, State};
 
 %% CopyData for Replication mode
-on_message({?COPY_DATA, <<?PRIMARY_KEEPALIVE_MESSAGE:8, LSN:?int64, _Timestamp:?int64, ReplyRequired:8>>},
-    #state{repl_last_flushed_lsn = LastFlushedLSN, repl_last_applied_lsn = LastAppliedLSN,
-        repl_align_lsn = AlignLsn} = State) ->
-    case ReplyRequired of
-        1 when AlignLsn ->
-            send(State, ?COPY_DATA,
-                epgsql_wire:encode_standby_status_update(LSN, LSN, LSN)),
-            {noreply, State#state{repl_feedback_required = false,
-                repl_last_received_lsn = LSN,
-                repl_last_applied_lsn = LSN,
-                repl_last_flushed_lsn = LSN}};
-        1 when not AlignLsn ->
-            send(State, ?COPY_DATA,
-                epgsql_wire:encode_standby_status_update(LSN, LastFlushedLSN, LastAppliedLSN)),
-            {noreply, State#state{repl_feedback_required = false, repl_last_received_lsn = LSN}};
-        _ ->
-            {noreply, State#state{repl_feedback_required = true, repl_last_received_lsn = LSN}}
-    end;
+on_replication(?COPY_DATA, <<?PRIMARY_KEEPALIVE_MESSAGE:8, LSN:?int64, _Timestamp:?int64, ReplyRequired:8>>,
+               #state{repl = #repl{last_flushed_lsn = LastFlushedLSN,
+                                   last_applied_lsn = LastAppliedLSN,
+                                   align_lsn = AlignLsn} = Repl} = State) ->
+    Repl1 =
+        case ReplyRequired of
+            1 when AlignLsn ->
+                send(State, ?COPY_DATA,
+                     epgsql_wire:encode_standby_status_update(LSN, LSN, LSN)),
+                Repl#repl{feedback_required = false,
+                     last_received_lsn = LSN, last_applied_lsn = LSN, last_flushed_lsn = LSN};
+            1 when not AlignLsn ->
+                send(State, ?COPY_DATA,
+                     epgsql_wire:encode_standby_status_update(LSN, LastFlushedLSN, LastAppliedLSN)),
+                Repl#repl{feedback_required = false,
+                          last_received_lsn = LSN};
+            _ ->
+                Repl#repl{feedback_required = true,
+                          last_received_lsn = LSN}
+        end,
+    {noreply, State#state{repl = Repl1}};
 
-%% CopyData for Replication mode. with async messages
-on_message({?COPY_DATA, <<?X_LOG_DATA, StartLSN:?int64, EndLSN:?int64, _Timestamp:?int64, WALRecord/binary>>},
-    #state{repl_cbmodule = undefined, repl_receiver = Receiver} = State) ->
+%% CopyData for Replication mode
+on_replication(?COPY_DATA, <<?X_LOG_DATA, StartLSN:?int64, EndLSN:?int64,
+                             _Timestamp:?int64, WALRecord/binary>>,
+               #state{repl = Repl} = State) ->
+    Repl1 = handle_xlog_data(StartLSN, EndLSN, WALRecord, Repl),
+    {noreply, State#state{repl = Repl1}};
+on_replication(?ERROR, Err, State) ->
+    Reason = epgsql_wire:decode_error(Err),
+    {stop, {error, Reason}, State};
+on_replication(M, Data, Sock) when M == ?NOTICE;
+                                   M == ?NOTIFICATION;
+                                   M == ?PARAMETER_STATUS ->
+    on_message(M, Data, Sock).
+
+
+handle_xlog_data(StartLSN, EndLSN, WALRecord, #repl{cbmodule = undefined,
+                                                    receiver = Receiver} = Repl) ->
+    %% with async messages
     Receiver ! {epgsql, self(), {x_log_data, StartLSN, EndLSN, WALRecord}},
-    {noreply, State#state{repl_feedback_required = true, repl_last_received_lsn = EndLSN}};
-
-%% CopyData for Replication mode. with callback method
-on_message({?COPY_DATA, <<?X_LOG_DATA, StartLSN:?int64, EndLSN:?int64, _Timestamp:?int64, WALRecord/binary>>},
-    #state{repl_cbmodule = CbModule, repl_cbstate = CbState, repl_receiver = undefined} = State) ->
+    Repl#repl{feedback_required = true,
+              last_received_lsn = EndLSN};
+handle_xlog_data(StartLSN, EndLSN, WALRecord,
+                 #repl{cbmodule = CbModule, cbstate = CbState, receiver = undefined} = Repl) ->
+    %% with callback method
     {ok, LastFlushedLSN, LastAppliedLSN, NewCbState} =
-        CbModule:handle_x_log_data(StartLSN, EndLSN, WALRecord, CbState),
-    {noreply, State#state{repl_feedback_required = true, repl_last_received_lsn = EndLSN,
-        repl_last_flushed_lsn = LastFlushedLSN, repl_last_applied_lsn = LastAppliedLSN,
-        repl_cbstate = NewCbState}}.
+        epgsql:handle_x_log_data(CbModule, StartLSN, EndLSN, WALRecord, CbState),
+    Repl#repl{feedback_required = true,
+              last_received_lsn = EndLSN,
+              last_flushed_lsn = LastFlushedLSN,
+              last_applied_lsn = LastAppliedLSN,
+              cbstate = NewCbState}.

+ 0 - 207
src/epgsql_types.erl

@@ -1,207 +0,0 @@
--module(epgsql_types).
-
--export([oid2type/1, type2oid/1]).
-
-oid2type(16)    -> bool;
-oid2type(17)    -> bytea;
-oid2type(18)    -> char;
-oid2type(19)    -> name;
-oid2type(20)    -> int8;
-oid2type(21)    -> int2;
-oid2type(22)    -> int2vector;
-oid2type(23)    -> int4;
-oid2type(24)    -> regproc;
-oid2type(25)    -> text;
-oid2type(26)    -> oid;
-oid2type(27)    -> tid;
-oid2type(28)    -> xid;
-oid2type(29)    -> cid;
-oid2type(30)    -> oidvector;
-oid2type(71)    -> pg_type_reltype;
-oid2type(75)    -> pg_attribute_reltype;
-oid2type(81)    -> pg_proc_reltype;
-oid2type(83)    -> pg_class_reltype;
-oid2type(114)   -> json;
-oid2type(142)   -> xml;
-oid2type(199)   -> {array, json};
-oid2type(600)   -> point;
-oid2type(601)   -> lseg;
-oid2type(602)   -> path;
-oid2type(603)   -> box;
-oid2type(604)   -> polygon;
-oid2type(628)   -> line;
-oid2type(700)   -> float4;
-oid2type(701)   -> float8;
-oid2type(702)   -> abstime;
-oid2type(703)   -> reltime;
-oid2type(704)   -> tinterval;
-oid2type(705)   -> unknown;
-oid2type(718)   -> circle;
-oid2type(790)   -> cash;
-oid2type(829)   -> macaddr;
-oid2type(869)   -> inet;
-oid2type(650)   -> cidr;
-oid2type(651)   -> {array, cidr};
-oid2type(1000)  -> {array, bool};
-oid2type(1005)  -> {array, int2};
-oid2type(1007)  -> {array, int4};
-oid2type(1009)  -> {array, text};
-oid2type(1014)  -> {array, char};
-oid2type(1015)  -> {array, varchar};
-oid2type(1016)  -> {array, int8};
-oid2type(1021)  -> {array, float4};
-oid2type(1022)  -> {array, float8};
-oid2type(1033)  -> aclitem;
-oid2type(1263)  -> {array, cstring};
-oid2type(1041)  -> {array, inet};
-oid2type(1042)  -> bpchar;
-oid2type(1043)  -> varchar;
-oid2type(1082)  -> date;
-oid2type(1083)  -> time;
-oid2type(1114)  -> timestamp;
-oid2type(1115)  -> {array, timestamp};
-oid2type(1182)  -> {array, date};
-oid2type(1183)  -> {array, time};
-oid2type(1184)  -> timestamptz;
-oid2type(1185)  -> {array, timestamptz};
-oid2type(1186)  -> interval;
-oid2type(1187)  -> {array, interval};
-oid2type(1266)  -> timetz;
-oid2type(1270)  -> {array, timetz};
-oid2type(1560)  -> bit;
-oid2type(1562)  -> varbit;
-oid2type(1700)  -> numeric;
-oid2type(1790)  -> refcursor;
-oid2type(2202)  -> regprocedure;
-oid2type(2203)  -> regoper;
-oid2type(2204)  -> regoperator;
-oid2type(2205)  -> regclass;
-oid2type(2206)  -> regtype;
-oid2type(2211)  -> {array, regtype};
-oid2type(3614)  -> tsvector;
-oid2type(3642)  -> gtsvector;
-oid2type(3615)  -> tsquery;
-oid2type(3734)  -> regconfig;
-oid2type(3769)  -> regdictionary;
-oid2type(2249)  -> record;
-oid2type(2275)  -> cstring;
-oid2type(2276)  -> any;
-oid2type(2277)  -> {array, any};
-oid2type(2278)  -> void;
-oid2type(2279)  -> trigger;
-oid2type(2280)  -> language_handler;
-oid2type(2281)  -> internal;
-oid2type(2282)  -> opaque;
-oid2type(2283)  -> anyelement;
-oid2type(2287)  -> {array, record};
-oid2type(2776)  -> anynonarray;
-oid2type(2950)  -> uuid;
-oid2type(2951)  -> {array, uuid};
-oid2type(3500)  -> anyenum;
-oid2type(3802)  -> jsonb;
-oid2type(3807)  -> {array, jsonb};
-oid2type(3904)  -> int4range;
-oid2type(3926)  -> int8range;
-oid2type(Oid)   -> {unknown_oid, Oid}.
-
-type2oid(bool)                  -> 16;
-type2oid(bytea)                 -> 17;
-type2oid(char)                  -> 18;
-type2oid(name)                  -> 19;
-type2oid(int8)                  -> 20;
-type2oid(int2)                  -> 21;
-type2oid(int2vector)            -> 22;
-type2oid(int4)                  -> 23;
-type2oid(regproc)               -> 24;
-type2oid(text)                  -> 25;
-type2oid(oid)                   -> 26;
-type2oid(tid)                   -> 27;
-type2oid(xid)                   -> 28;
-type2oid(cid)                   -> 29;
-type2oid(oidvector)             -> 30;
-type2oid(pg_type_reltype)       -> 71;
-type2oid(pg_attribute_reltype)  -> 75;
-type2oid(pg_proc_reltype)       -> 81;
-type2oid(pg_class_reltype)      -> 83;
-type2oid(json)                  -> 114;
-type2oid(xml)                   -> 142;
-type2oid({array, json})         -> 199;
-type2oid(point)                 -> 600;
-type2oid(lseg)                  -> 601;
-type2oid(path)                  -> 602;
-type2oid(box)                   -> 603;
-type2oid(polygon)               -> 604;
-type2oid(line)                  -> 628;
-type2oid(cidr)                  -> 650;
-type2oid({array, cidr})         -> 651;
-type2oid(float4)                -> 700;
-type2oid(float8)                -> 701;
-type2oid(abstime)               -> 702;
-type2oid(reltime)               -> 703;
-type2oid(tinterval)             -> 704;
-type2oid(unknown)               -> 705;
-type2oid(circle)                -> 718;
-type2oid(cash)                  -> 790;
-type2oid(macaddr)               -> 829;
-type2oid(inet)                  -> 869;
-type2oid({array, bool})         -> 1000;
-type2oid({array, int2})         -> 1005;
-type2oid({array, int4})         -> 1007;
-type2oid({array, text})         -> 1009;
-type2oid({array, char})         -> 1014;
-type2oid({array, varchar})      -> 1015;
-type2oid({array, int8})         -> 1016;
-type2oid({array, float4})       -> 1021;
-type2oid({array, float8})       -> 1022;
-type2oid(aclitem)               -> 1033;
-type2oid({array, cstring})      -> 1263;
-type2oid({array, inet})         -> 1041;
-type2oid(bpchar)                -> 1042;
-type2oid(varchar)               -> 1043;
-type2oid(date)                  -> 1082;
-type2oid(time)                  -> 1083;
-type2oid(timestamp)             -> 1114;
-type2oid({array, timestamp})    -> 1115;
-type2oid({array, date})         -> 1182;
-type2oid({array, time})         -> 1183;
-type2oid(timestamptz)           -> 1184;
-type2oid({array, timestamptz})  -> 1185;
-type2oid(interval)              -> 1186;
-type2oid({array, interval})     -> 1187;
-type2oid(timetz)                -> 1266;
-type2oid({array, timetz})       -> 1270;
-type2oid(bit)                   -> 1560;
-type2oid(varbit)                -> 1562;
-type2oid(numeric)               -> 1700;
-type2oid(refcursor)             -> 1790;
-type2oid(regprocedure)          -> 2202;
-type2oid(regoper)               -> 2203;
-type2oid(regoperator)           -> 2204;
-type2oid(regclass)              -> 2205;
-type2oid(regtype)               -> 2206;
-type2oid({array, regtype})      -> 2211;
-type2oid(tsvector)              -> 3614;
-type2oid(gtsvector)             -> 3642;
-type2oid(tsquery)               -> 3615;
-type2oid(regconfig)             -> 3734;
-type2oid(regdictionary)         -> 3769;
-type2oid(record)                -> 2249;
-type2oid(cstring)               -> 2275;
-type2oid(any)                   -> 2276;
-type2oid({array, any})          -> 2277;
-type2oid(void)                  -> 2278;
-type2oid(trigger)               -> 2279;
-type2oid(language_handler)      -> 2280;
-type2oid(internal)              -> 2281;
-type2oid(opaque)                -> 2282;
-type2oid(anyelement)            -> 2283;
-type2oid({array, record})       -> 2287;
-type2oid(anynonarray)           -> 2776;
-type2oid(uuid)                  -> 2950;
-type2oid({array, uuid})         -> 2951;
-type2oid(anyenum)               -> 3500;
-type2oid(jsonb)                 -> 3802;
-type2oid({array, jsonb})        -> 3807;
-type2oid(int4range)             -> 3904;
-type2oid(int8range)             -> 3926;
-type2oid(Type)                  -> {unknown_type, Type}.

+ 124 - 82
src/epgsql_wire.erl

@@ -7,46 +7,54 @@
          decode_error/1,
          decode_strings/1,
          decode_columns/3,
-         encode/1,
-         encode/2,
-         decode_data/3,
+         decode_parameters/2,
+         encode_command/1,
+         encode_command/2,
+         build_decoder/2,
+         decode_data/2,
          decode_complete/1,
          encode_types/2,
          encode_formats/1,
-         format/1,
+         format/2,
          encode_parameters/2,
          encode_standby_status_update/3]).
+-export_type([row_decoder/0]).
 
 -include("epgsql.hrl").
--include("epgsql_binary.hrl").
+-include("protocol.hrl").
 
+-opaque row_decoder() :: {[epgsql_binary:decoder()], [epgsql:column()], epgsql_binary:codec()}.
+
+-spec decode_message(binary()) -> {byte(), binary(), binary()} | binary().
 decode_message(<<Type:8, Len:?int32, Rest/binary>> = Bin) ->
     Len2 = Len - 4,
     case Rest of
         <<Data:Len2/binary, Tail/binary>> ->
-            case Type of
-                $E ->
-                    {{error, decode_error(Data)}, Tail};
-                _ ->
-                    {{Type, Data}, Tail}
-            end;
+            {Type, Data, Tail};
         _Other ->
             Bin
     end;
-
 decode_message(Bin) ->
     Bin.
 
-%% decode a single null-terminated string
+%% @doc decode a single null-terminated string
+-spec decode_string(binary()) -> [binary(), ...].
 decode_string(Bin) ->
     binary:split(Bin, <<0>>).
 
-%% decode multiple null-terminated string
+%% @doc decode multiple null-terminated string
+-spec decode_strings(binary()) -> [binary(), ...].
 decode_strings(Bin) ->
-    [<<>> | T] = lists:reverse(binary:split(Bin, <<0>>, [global])),
-    lists:reverse(T).
-
-%% decode field
+    %% Assert the last byte is what we want it to be
+    %% Remove that byte from the Binary, so the zero
+    %% terminators are separators. Then apply
+    %% binary:split/3 directly on the remaining Subj
+    Sz = byte_size(Bin) - 1,
+    <<Subj:Sz/binary, 0>> = Bin,
+    binary:split(Subj, <<0>>, [global]).
+
+%% @doc decode error's field
+-spec decode_fields(binary()) -> [{byte(), binary()}].
 decode_fields(Bin) ->
     decode_fields(Bin, []).
 
@@ -56,8 +64,9 @@ decode_fields(<<Type:8, Rest/binary>>, Acc) ->
     [Str, Rest2] = decode_string(Rest),
     decode_fields(Rest2, [{Type, Str} | Acc]).
 
-%% decode ErrorResponse
+%% @doc decode ErrorResponse
 %% See http://www.postgresql.org/docs/current/interactive/protocol-error-fields.html
+-spec decode_error(binary()) -> epgsql:query_error().
 decode_error(Bin) ->
     Fields = decode_fields(Bin),
     ErrCode = proplists:get_value($C, Fields),
@@ -113,50 +122,60 @@ lower_atom(Str) when is_binary(Str) ->
 lower_atom(Str) when is_list(Str) ->
     list_to_atom(string:to_lower(Str)).
 
-encode(Data) ->
-    Bin = iolist_to_binary(Data),
-    <<(byte_size(Bin) + 4):?int32, Bin/binary>>.
-
-encode(Type, Data) ->
-    Bin = iolist_to_binary(Data),
-    <<Type:8, (byte_size(Bin) + 4):?int32, Bin/binary>>.
-
-%% decode data
-decode_data(Columns, Bin, Codec) ->
-    decode_data(Columns, Bin, [], Codec).
-
-decode_data([], _Bin, Acc, _Codec) ->
-    list_to_tuple(lists:reverse(Acc));
-decode_data([_C | T], <<-1:?int32, Rest/binary>>, Acc, Codec) ->
-    decode_data(T, Rest, [null | Acc], Codec);
-decode_data([C | T], <<Len:?int32, Value:Len/binary, Rest/binary>>, Acc, Codec) ->
-    Value2 = case C of
-        #column{type = Type, format = 1} ->
-            epgsql_binary:decode(Type, Value, Codec);
-        #column{} ->
-            Value
-    end,
-    decode_data(T, Rest, [Value2 | Acc], Codec).
 
-%% decode column information
+%% @doc Build decoder for DataRow
+-spec build_decoder([epgsql:column()], epgsql_binary:codec()) -> row_decoder().
+build_decoder(Columns, Codec) ->
+    Decoders = lists:map(
+                 fun(#column{oid = Oid, format = Format}) ->
+                         Fmt = case Format of
+                                   1 -> binary;
+                                   0 -> text
+                               end,
+                         epgsql_binary:oid_to_decoder(Oid, Fmt, Codec)
+                 end, Columns),
+    {Decoders, Columns, Codec}.
+
+%% @doc decode row data
+-spec decode_data(binary(), row_decoder()) -> tuple().
+decode_data(Bin, {Decoders, _Columns, Codec}) ->
+    list_to_tuple(decode_data(Bin, Decoders, Codec)).
+
+decode_data(_, [], _) -> [];
+decode_data(<<-1:?int32, Rest/binary>>, [_Dec | Decs], Codec) ->
+    [null | decode_data(Rest, Decs, Codec)];
+decode_data(<<Len:?int32, Value:Len/binary, Rest/binary>>, [Decoder | Decs], Codec) ->
+    [epgsql_binary:decode(Value, Decoder)
+     | decode_data(Rest, Decs, Codec)].
+
+%% @doc decode column information
+-spec decode_columns(non_neg_integer(), binary(), epgsql_binary:codec()) -> [epgsql:column()].
+decode_columns(0, _Bin, _Codec) -> [];
 decode_columns(Count, Bin, Codec) ->
-    decode_columns(Count, Bin, [], Codec).
-
-decode_columns(0, _Bin, Acc, _Codec) ->
-    lists:reverse(Acc);
-decode_columns(N, Bin, Acc, Codec) ->
     [Name, Rest] = decode_string(Bin),
-    <<_Table_Oid:?int32, _Attrib_Num:?int16, Type_Oid:?int32,
-     Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest,
+    <<_TableOid:?int32, _AttribNum:?int16, TypeOid:?int32,
+      Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest,
+    %% TODO: get rid of this 'type' (extra oid_db lookup)
+    Type = epgsql_binary:oid_to_name(TypeOid, Codec),
     Desc = #column{
       name     = Name,
-      type     = epgsql_binary:oid2type(Type_Oid, Codec),
+      type     = Type,
+      oid      = TypeOid,
       size     = Size,
       modifier = Modifier,
       format   = Format},
-    decode_columns(N - 1, Rest2, [Desc | Acc], Codec).
-
-%% decode command complete msg
+    [Desc | decode_columns(Count - 1, Rest2, Codec)].
+
+%% @doc decode ParameterDescription
+-spec decode_parameters(binary(), epgsql_binary:codec()) ->
+                               [epgsql_oid_db:type_info() | {unknown_oid, epgsql_oid_db:oid()}].
+decode_parameters(<<_Count:?int16, Bin/binary>>, Codec) ->
+    [case epgsql_binary:oid_to_info(Oid, Codec)  of
+         undefined -> {unknown_oid, Oid};
+         TypeInfo -> TypeInfo
+     end || <<Oid:?int32>> <= Bin].
+
+%% @doc decode command complete msg
 decode_complete(<<"SELECT", 0>>)        -> select;
 decode_complete(<<"SELECT", _/binary>>) -> select;
 decode_complete(<<"BEGIN", 0>>)         -> 'begin';
@@ -172,7 +191,8 @@ decode_complete(Bin) ->
         [Type | _Rest]         -> lower_atom(Type)
     end.
 
-%% encode types
+
+%% @doc encode types
 encode_types(Types, Codec) ->
     encode_types(Types, 0, <<>>, Codec).
 
@@ -181,12 +201,13 @@ encode_types([], Count, Acc, _Codec) ->
 
 encode_types([Type | T], Count, Acc, Codec) ->
     Oid = case Type of
-        undefined -> 0;
-        _Any      -> epgsql_binary:type2oid(Type, Codec)
+              undefined -> 0;
+              _Any -> epgsql_binary:type_to_oid(Type, Codec)
     end,
     encode_types(T, Count + 1, <<Acc/binary, Oid:?int32>>, Codec).
 
-%% encode column formats
+%% @doc encode expected column formats
+-spec encode_formats([epgsql:column()]) -> binary().
 encode_formats(Columns) ->
     encode_formats(Columns, 0, <<>>).
 
@@ -196,45 +217,66 @@ encode_formats([], Count, Acc) ->
 encode_formats([#column{format = Format} | T], Count, Acc) ->
     encode_formats(T, Count + 1, <<Acc/binary, Format:?int16>>).
 
-format(Type) ->
-    case epgsql_binary:supports(Type) of
-        true  -> 1;
-        false -> 0
+format({unknown_oid, _}, _) -> 0;
+format(#column{oid = Oid}, Codec) ->
+    case epgsql_binary:supports(Oid, Codec) of
+        true  -> 1;                             %binary
+        false -> 0                              %text
     end.
 
-%% encode parameters
+%% @doc encode parameters for 'Bind'
+-spec encode_parameters([], epgsql_binary:codec()) -> iolist().
 encode_parameters(Parameters, Codec) ->
-    encode_parameters(Parameters, 0, <<>>, <<>>, Codec).
+    encode_parameters(Parameters, 0, <<>>, [], Codec).
 
 encode_parameters([], Count, Formats, Values, _Codec) ->
-    <<Count:?int16, Formats/binary, Count:?int16, Values/binary>>;
+    [<<Count:?int16>>, Formats, <<Count:?int16>> | lists:reverse(Values)];
 
 encode_parameters([P | T], Count, Formats, Values, Codec) ->
     {Format, Value} = encode_parameter(P, Codec),
     Formats2 = <<Formats/binary, Format:?int16>>,
-    Values2 = <<Values/binary, Value/binary>>,
+    Values2 = [Value | Values],
     encode_parameters(T, Count + 1, Formats2, Values2, Codec).
 
-%% encode parameter
-
+%% @doc encode single 'typed' parameter
+-spec encode_parameter({Type, Val :: any()},
+                       epgsql_binary:codec()) -> {0..1, iolist()} when
+      Type :: epgsql:type_name()
+            | {array, epgsql:type_name()}
+            | {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) ->
-    case epgsql_binary:encode(Type, Value, Codec) of
-        Bin when is_binary(Bin) -> {1, Bin};
-        {error, unsupported}    -> encode_parameter(Value)
-    end;
-encode_parameter(Value, _Codec) -> encode_parameter(Value).
+    {1, epgsql_binary:encode(Type, Value, Codec)};
+encode_parameter(Value, _Codec) ->
+    {0, encode_text(Value)}.
 
-encode_parameter(A) when is_atom(A)    -> {0, encode_list(atom_to_list(A))};
-encode_parameter(B) when is_binary(B)  -> {0, <<(byte_size(B)):?int32, B/binary>>};
-encode_parameter(I) when is_integer(I) -> {0, encode_list(integer_to_list(I))};
-encode_parameter(F) when is_float(F)   -> {0, encode_list(float_to_list(F))};
-encode_parameter(L) when is_list(L)    -> {0, encode_list(L)}.
+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(I) when is_integer(I) -> encode_bin(integer_to_binary(I));
+encode_text(F) when is_float(F)   -> encode_bin(float_to_binary(F));
+encode_text(L) when is_list(L)    -> encode_bin(list_to_binary(L)).
 
-encode_list(L) ->
-    Bin = list_to_binary(L),
+encode_bin(Bin) ->
     <<(byte_size(Bin)):?int32, Bin/binary>>.
 
+%% @doc Encode iodata with size-prefix (used for `StartupMessage' and `SSLRequest' packets)
+encode_command(Data) ->
+    Size = iolist_size(Data),
+    [<<(Size + 4):?int32>> | Data].
+
+%% @doc Encode PG command with type and size prefix
+encode_command(Type, Data) ->
+    Size = iolist_size(Data),
+    [<<Type:8, (Size + 4):?int32>> | Data].
+
+%% @doc encode replication status message
 encode_standby_status_update(ReceivedLSN, FlushedLSN, AppliedLSN) ->
     {MegaSecs, Secs, MicroSecs} = os:timestamp(),
-    Timestamp = ((MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs) - 946684800*1000000, %% microseconds since midnight on 2000-01-01
+    %% microseconds since midnight on 2000-01-01
+    Timestamp = ((MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs) - 946684800*1000000,
     <<$r:8, ReceivedLSN:?int64, FlushedLSN:?int64, AppliedLSN:?int64, Timestamp:?int64, 0:8>>.

+ 49 - 41
src/epgsqla.erl

@@ -19,7 +19,7 @@
          close/2, close/3,
          sync/1,
          cancel/1,
-         complete_connect/2]).
+         complete_connect/3]).
 
 -include("epgsql.hrl").
 
@@ -29,11 +29,8 @@ start_link() ->
     epgsql_sock:start_link().
 
 connect(Opts) ->
-    Settings = epgsql:to_proplist(Opts),
-    Host = proplists:get_value(host, Settings, "localhost"),
-    Username = proplists:get_value(username, Settings, os:getenv("USER")),
-    Password = proplists:get_value(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -46,9 +43,20 @@ connect(Host, Username, Password, Opts) ->
     connect(C, Host, Username, Password, Opts).
 
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
-              string(), string(), [epgsql:connect_option()]) -> reference().
+              string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
-    complete_connect(C, cast(C, {connect, Host, Username, Password, epgsql:to_proplist(Opts)})).
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+-spec call_connect(epgsql:connection(), epgsql:connect_opts()) -> reference().
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(epgsql:to_map(Opts)),
+    complete_connect(
+      C, cast(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
@@ -69,20 +77,20 @@ set_notice_receiver(C, PidOrName) ->
 get_cmd_status(C) ->
     epgsql_sock:get_cmd_status(C).
 
--spec squery(epgsql:connection(), string()) -> reference().
+-spec squery(epgsql:connection(), epgsql:sql_query()) -> reference().
 squery(C, Sql) ->
-    cast(C, {squery, Sql}).
+    cast(C, epgsql_cmd_squery, Sql).
 
 equery(C, Sql) ->
     equery(C, Sql, []).
 
--spec equery(epgsql:connection(), #statement{}, [epgsql:typed_param()]) -> reference().
+-spec equery(epgsql:connection(), epgsql:statement(), [epgsql:typed_param()]) -> reference().
 equery(C, Statement, TypedParameters) ->
-    cast(C, {equery, Statement, TypedParameters}).
+    cast(C, epgsql_cmd_equery, {Statement, TypedParameters}).
 
--spec prepared_query(epgsql:connection(), #statement{}, [epgsql:typed_param()]) -> reference().
+-spec prepared_query(epgsql:connection(), epgsql:statement(), [epgsql:typed_param()]) -> reference().
 prepared_query(C, Statement, TypedParameters) ->
-    cast(C, {prepared_query, Statement, TypedParameters}).
+    cast(C, epgsql_cmd_prepared_query, {Statement, TypedParameters}).
 
 parse(C, Sql) ->
     parse(C, "", Sql, []).
@@ -90,16 +98,16 @@ parse(C, Sql) ->
 parse(C, Sql, Types) ->
     parse(C, "", Sql, Types).
 
--spec parse(epgsql:connection(), iolist(), string(), [epgsql_type()]) -> reference().
+-spec parse(epgsql:connection(), iolist(), epgsql:sql_query(), [epgsql:epgsql_type()]) -> reference().
 parse(C, Name, Sql, Types) ->
-    cast(C, {parse, Name, Sql, Types}).
+    cast(C, epgsql_cmd_parse, {Name, Sql, Types}).
 
 bind(C, Statement, Parameters) ->
     bind(C, Statement, "", Parameters).
 
--spec bind(epgsql:connection(), #statement{}, string(), [epgsql:bind_param()]) -> reference().
+-spec bind(epgsql:connection(), epgsql:statement(), string(), [epgsql:bind_param()]) -> reference().
 bind(C, Statement, PortalName, Parameters) ->
-    cast(C, {bind, Statement, PortalName, Parameters}).
+    cast(C, epgsql_cmd_bind, {Statement, PortalName, Parameters}).
 
 execute(C, S) ->
     execute(C, S, "", 0).
@@ -107,31 +115,31 @@ execute(C, S) ->
 execute(C, S, N) ->
     execute(C, S, "", N).
 
--spec execute(epgsql:connection(), #statement{}, string(), non_neg_integer()) -> reference().
+-spec execute(epgsql:connection(), epgsql:statement(), string(), non_neg_integer()) -> reference().
 execute(C, Statement, PortalName, MaxRows) ->
-    cast(C, {execute, Statement, PortalName, MaxRows}).
+    cast(C, epgsql_cmd_execute, {Statement, PortalName, MaxRows}).
 
--spec execute_batch(epgsql:connection(), [{#statement{}, [epgsql:bind_param()]}]) -> reference().
+-spec execute_batch(epgsql:connection(), [{epgsql:statement(), [epgsql:bind_param()]}]) -> reference().
 execute_batch(C, Batch) ->
-    cast(C, {execute_batch, Batch}).
+    cast(C, epgsql_cmd_batch, Batch).
 
 describe(C, #statement{name = Name}) ->
     describe(C, statement, Name).
 
 describe(C, statement, Name) ->
-    cast(C, {describe_statement, Name});
+    cast(C, epgsql_cmd_describe_statement, Name);
 
 describe(C, portal, Name) ->
-    cast(C, {describe_portal, Name}).
+    cast(C, epgsql_cmd_describe_portal, Name).
 
 close(C, #statement{name = Name}) ->
     close(C, statement, Name).
 
 close(C, Type, Name) ->
-    cast(C, {close, Type, Name}).
+    cast(C, epgsql_cmd_close, {Type, Name}).
 
 sync(C) ->
-    cast(C, sync).
+    cast(C, epgsql_cmd_sync, []).
 
 -spec cancel(epgsql:connection()) -> ok.
 cancel(C) ->
@@ -139,25 +147,25 @@ cancel(C) ->
 
 %% -- internal functions --
 
-cast(C, Command) ->
-    Ref = make_ref(),
-    gen_server:cast(C, {{cast, self(), Ref}, Command}),
-    Ref.
+cast(C, Command, Args) ->
+    epgsql_sock:async_command(C, cast, Command, Args).
 
-complete_connect(C, Ref) ->
+complete_connect(C, Ref, Opts) ->
     receive
         %% If we connect, then try and update the type cache.  When
         %% all is said and done, pass the result along as a message.
-        {C, Ref, Msg} ->
-            Retval =
-                case Msg of
-                    connected ->
-                        ok = epgsql:update_type_cache(C),
-                        {C, Ref, connected};
-                    {error, Error} ->
-                        {C, Ref, {error, Error}}
-                end,
-            self() ! Retval;
+        {C, Ref, connected} ->
+            case maps:get(codecs, Opts, undefined) of
+                undefined ->
+                    {ok, _} = epgsql:update_type_cache(C);
+                [_|_] = Codecs ->
+                    {ok, _} = epgsql:update_type_cache(C, Codecs);
+                [] ->
+                    ok
+            end,
+            self() ! {C, Ref, connected};
+        {C, Ref, {error, _} = Err} ->
+            self() ! {C, Ref, Err};
         {'EXIT', C, Reason} ->
             self() ! {'EXIT', C, Reason}
     end,

+ 41 - 34
src/epgsqli.erl

@@ -28,11 +28,8 @@ start_link() ->
     epgsql_sock:start_link().
 
 connect(Opts) ->
-    Settings = epgsql:to_proplist(Opts),
-    Host = proplists:get_value(host, Settings, "localhost"),
-    Username = proplists:get_value(username, Settings, os:getenv("USER")),
-    Password = proplists:get_value(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -45,9 +42,19 @@ connect(Host, Username, Password, Opts) ->
     connect(C, Host, Username, Password, Opts).
 
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
-              string(), string(), [epgsql:connect_option()]) -> reference().
+              string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
-    epgsqla:complete_connect(C, incremental(C, {connect, Host, Username, Password, epgsql:to_proplist(Opts)})).
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(epgsql:to_map(Opts)),
+    epgsqla:complete_connect(
+      C, incremental(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
@@ -62,43 +69,45 @@ get_parameter(C, Name) ->
 set_notice_receiver(C, PidOrName) ->
     epgsql_sock:set_notice_receiver(C, PidOrName).
 
--spec get_cmd_status(epgsql:connection()) -> {ok, Status}
-                                          when
-      Status :: undefined | atom() | {atom(), integer()}.
+-spec get_cmd_status(epgsql:connection()) -> {ok, Status} when
+          Status :: undefined | atom() | {atom(), integer()}.
 get_cmd_status(C) ->
     epgsql_sock:get_cmd_status(C).
 
--spec squery(epgsql:connection(), string()) -> reference().
+-spec squery(epgsql:connection(), epgsql:sql_query()) -> reference().
 squery(C, Sql) ->
-    incremental(C, {squery, Sql}).
+    incremental(C, epgsql_cmd_squery, Sql).
 
-equery(C, Sql) ->
-    equery(C, Sql, []).
+-spec equery(epgsql:connection(), epgsql:statement()) -> reference().
+equery(C, Statement) ->
+    equery(C, Statement, []).
 
--spec equery(epgsql:connection(), #statement{}, [epgsql:typed_param()]) -> reference().
+-spec equery(epgsql:connection(), epgsql:statement(), [epgsql:typed_param()]) -> reference().
 equery(C, Statement, TypedParameters) ->
-    incremental(C, {equery, Statement, TypedParameters}).
+    incremental(C, epgsql_cmd_equery, {Statement, TypedParameters}).
 
--spec prepared_query(epgsql:connection(), #statement{}, [epgsql:typed_param()]) -> reference().
+-spec prepared_query(epgsql:connection(), epgsql:statement(), [epgsql:typed_param()]) -> reference().
 prepared_query(C, Statement, TypedParameters) ->
-    incremental(C, {prepared_query, Statement, TypedParameters}).
+    incremental(C, epgsql_cmd_prepared_query, {Statement, TypedParameters}).
 
+-spec parse(epgsql:connection(), epgsql:sql_query()) -> reference().
 parse(C, Sql) ->
     parse(C, "", Sql, []).
 
+-spec parse(epgsql:connection(), epgsql:sql_query(), [epgsql:epgsql_type()]) -> reference().
 parse(C, Sql, Types) ->
     parse(C, "", Sql, Types).
 
--spec parse(epgsql:connection(), iolist(), string(), [epgsql_type()]) -> reference().
+-spec parse(epgsql:connection(), iolist(), epgsql:sql_query(), [epgsql:epgsql_type()]) -> reference().
 parse(C, Name, Sql, Types) ->
-    incremental(C, {parse, Name, Sql, Types}).
+    incremental(C, epgsql_cmd_parse, {Name, Sql, Types}).
 
 bind(C, Statement, Parameters) ->
     bind(C, Statement, "", Parameters).
 
--spec bind(epgsql:connection(), #statement{}, string(), [epgsql:bind_param()]) -> reference().
+-spec bind(epgsql:connection(), epgsql:statement(), string(), [epgsql:bind_param()]) -> reference().
 bind(C, Statement, PortalName, Parameters) ->
-    incremental(C, {bind, Statement, PortalName, Parameters}).
+    incremental(C, epgsql_cmd_bind, {Statement, PortalName, Parameters}).
 
 execute(C, S) ->
     execute(C, S, "", 0).
@@ -106,31 +115,31 @@ execute(C, S) ->
 execute(C, S, N) ->
     execute(C, S, "", N).
 
--spec execute(epgsql:connection(), #statement{}, string(), non_neg_integer()) -> reference().
+-spec execute(epgsql:connection(), epgsql:statement(), string(), non_neg_integer()) -> reference().
 execute(C, Statement, PortalName, MaxRows) ->
-    incremental(C, {execute, Statement, PortalName, MaxRows}).
+    incremental(C, epgsql_cmd_execute, {Statement, PortalName, MaxRows}).
 
--spec execute_batch(epgsql:connection(), [{#statement{}, [epgsql:bind_param()]}]) -> reference().
+-spec execute_batch(epgsql:connection(), [{epgsql:statement(), [epgsql:bind_param()]}]) -> reference().
 execute_batch(C, Batch) ->
-    incremental(C, {execute_batch, Batch}).
+    incremental(C, epgsql_cmd_batch, Batch).
 
 describe(C, #statement{name = Name}) ->
     describe(C, statement, Name).
 
 describe(C, statement, Name) ->
-    incremental(C, {describe_statement, Name});
+    incremental(C, epgsql_cmd_describe_statement, Name);
 
 describe(C, portal, Name) ->
-    incremental(C, {describe_portal, Name}).
+    incremental(C, epgsql_cmd_describe_portal, Name).
 
 close(C, #statement{name = Name}) ->
     close(C, statement, Name).
 
 close(C, Type, Name) ->
-    incremental(C, {close, Type, Name}).
+    incremental(C, epgsql_cmd_close, {Type, Name}).
 
 sync(C) ->
-    incremental(C, sync).
+    incremental(C, epgsql_cmd_sync, []).
 
 -spec cancel(epgsql:connection()) -> ok.
 cancel(C) ->
@@ -139,7 +148,5 @@ cancel(C) ->
 
 %% -- internal functions --
 
-incremental(C, Command) ->
-    Ref = make_ref(),
-    gen_server:cast(C, {{incremental, self(), Ref}, Command}),
-    Ref.
+incremental(C, Command, Args) ->
+    epgsql_sock:async_command(C, incremental, Command, Args).

+ 79 - 36
src/ewkb.erl

@@ -1,7 +1,50 @@
+%% https://en.wikipedia.org/wiki/Well-known_text
+%% http://postgis.net/docs/manual-2.4/using_postgis_dbmanagement.html#EWKB_EWKT
 -module(ewkb).
--include("epgsql_geometry.hrl").
 -export([decode_geometry/1, encode_geometry/1]).
+-export_type([point_type/0, point/1, multi_point/1, line_string/1,
+              multi_line_string/1, basic_string/1, curve/1, multi_curve/1,
+              polygon/1, multi_polygon/1, triangle/1, curve_polygon/1,
+              polyhedral_surface/1, surface/1, multi_surface/1, tin/1,
+              geometry/1, geometry/0, geometry_collection/1, geom_type/0]).
+
+-include("epgsql_geometry.hrl").
 
+-type point_type() :: '2d' | '3d' | '2dm' | '3dm'.
+-type point(PointType) :: #point{ point_type :: PointType }.
+-type multi_point(PointType) :: #multi_point{ point_type :: PointType }.
+-type line_string(PointType) :: #line_string{ point_type :: PointType }.
+-type multi_line_string(PointType) :: #multi_line_string{ point_type :: PointType }.
+-type basic_string(PointType) :: #circular_string{ point_type :: PointType }
+                               | #line_string{ point_type :: PointType }.
+-type curve(PointType) :: #circular_string{ point_type :: PointType }
+                        | #line_string{ point_type :: PointType }
+                        | #compound_curve{ point_type :: PointType }.
+-type multi_curve(PointType) :: #multi_curve{ point_type :: PointType }.
+-type polygon(PointType) :: #polygon{ point_type :: PointType }.
+-type multi_polygon(PointType) :: #multi_polygon{ point_type :: PointType }.
+-type triangle(PointType) :: #triangle{ point_type :: PointType }.
+-type curve_polygon(PointType) :: #curve_polygon{ point_type :: PointType }.
+-type polyhedral_surface(PointType) :: #polyhedral_surface{ point_type :: PointType }.
+-type surface(PointType) :: polygon(PointType)
+                          | curve_polygon(PointType)
+                          | polyhedral_surface(PointType).
+-type multi_surface(PointType) :: #multi_surface{ point_type :: PointType }.
+-type tin(PointType) :: #tin{ point_type :: PointType }.
+-type geometry(PointType) :: point(PointType) |
+                             line_string(PointType) |
+                             triangle(PointType) |
+                             tin(PointType) |
+                             curve(PointType) |
+                             surface(PointType) |
+                             multi_point(PointType) |
+                             multi_line_string(PointType) |
+                             multi_polygon(PointType) |
+                             multi_curve(PointType) |
+                             multi_surface(PointType) |
+                             geometry_collection(PointType).
+-type geometry() :: geometry(point_type()).
+-type geometry_collection(PointType) :: [geometry(PointType)].
 
 -type geom_type() :: geometry
                    | point       %
@@ -22,11 +65,12 @@
                    | tin%
                    | triangle.%
 
-
+-spec decode_geometry(binary()) -> geometry().
 decode_geometry(Binary) ->
   {Geometry, <<>>} = decode_geometry_data(Binary),
   Geometry.
 
+-spec encode_geometry(geometry()) -> binary().
 encode_geometry(Geometry) ->
   Type = encode_type(Geometry),
   PointType = encode_point_type(Geometry),
@@ -60,7 +104,7 @@ encode_geometry_data({SimpleCollection, _, Data})
          SimpleCollection == triangle ->
   encode_collection(Data);
 encode_geometry_data({TypedCollection, _, Data})
-    when 
+    when
         TypedCollection == multi_point;
         TypedCollection == multi_line_string;
         TypedCollection == multi_curve;
@@ -95,8 +139,8 @@ encode_typed_collection(Collection) when is_list(Collection) ->
     end,
     <<>>,
     Collection),
-  <<LengthBin/binary, CollectionBin/binary>>.  
-  
+  <<LengthBin/binary, CollectionBin/binary>>.
+
 
 encode_int32(Int) when is_integer(Int) ->
   <<Int:1/little-integer-unit:32>>.
@@ -117,8 +161,8 @@ decode_geometry_data(surface, _, _) -> error({surface, not_supported});
 decode_geometry_data(geometry, _, _) -> error({geometry, not_supported});
 decode_geometry_data(point, PointType, Data) ->
   decode_point(PointType, Data);
-decode_geometry_data(LineType, PointType, Data) 
-    when LineType == line_string; 
+decode_geometry_data(LineType, PointType, Data)
+    when LineType == line_string;
          LineType == circular_string ->
   {Points, Rest} = decode_collection(point, PointType, Data),
   {{LineType, PointType, Points}, Rest};
@@ -129,7 +173,7 @@ decode_geometry_data(triangle, PointType, Data) ->
   {#polygon{ rings = Rings }, Rest} = decode_geometry_data(polygon, PointType, Data),
   {#triangle{ point_type = PointType, rings = Rings }, Rest};
 decode_geometry_data(Collection, PointType, Data)
-    when 
+    when
         Collection == multi_point;
         Collection == multi_line_string;
         Collection == multi_curve;
@@ -185,13 +229,13 @@ decode_point(PointType, Data) ->
     {[], Data},
     lists:seq(1, point_size(PointType))),
   Point = case {PointType, Values} of
-    {'2d', [X,Y]} ->
+    {'2d', [X, Y]} ->
       #point{ point_type = PointType, x = X, y = Y };
-    {'2dm', [X,Y,M]} ->
+    {'2dm', [X, Y, M]} ->
       #point{ point_type = PointType, x = X, y = Y, m = M };
-    {'3d', [X,Y,Z]} ->
+    {'3d', [X, Y, Z]} ->
       #point{ point_type = PointType, x = X, y = Y, z = Z };
-    {'3dm', [X,Y,Z,M]} ->
+    {'3dm', [X, Y, Z, M]} ->
       #point{ point_type = PointType, x = X, y = Y, z = Z, m = M }
   end,
   {Point, Rest}.
@@ -203,24 +247,24 @@ point_size('3d')  -> 3;
 point_size('3dm') -> 4.
 
 -spec decode_type(binary()) -> geom_type().
-decode_type(<<0,0>>) -> geometry;
-decode_type(<<1,0>>) -> point;
-decode_type(<<2,0>>) -> line_string;
-decode_type(<<3,0>>) -> polygon;
-decode_type(<<4,0>>) -> multi_point;
-decode_type(<<5,0>>) -> multi_line_string;
-decode_type(<<6,0>>) -> multi_polygon;
-decode_type(<<7,0>>) -> geometry_collection;
-decode_type(<<8,0>>) -> circular_string;
-decode_type(<<9,0>>) -> compound_curve;
-decode_type(<<10,0>>) -> curve_polygon;
-decode_type(<<11,0>>) -> multi_curve;
-decode_type(<<12,0>>) -> multi_surface;
-decode_type(<<13,0>>) -> curve;
-decode_type(<<14,0>>) -> surface;
-decode_type(<<15,0>>) -> polyhedral_surface;
-decode_type(<<16,0>>) -> tin;
-decode_type(<<17,0>>) -> triangle.
+decode_type(<<0, 0>>) -> geometry;
+decode_type(<<1, 0>>) -> point;
+decode_type(<<2, 0>>) -> line_string;
+decode_type(<<3, 0>>) -> polygon;
+decode_type(<<4, 0>>) -> multi_point;
+decode_type(<<5, 0>>) -> multi_line_string;
+decode_type(<<6, 0>>) -> multi_polygon;
+decode_type(<<7, 0>>) -> geometry_collection;
+decode_type(<<8, 0>>) -> circular_string;
+decode_type(<<9, 0>>) -> compound_curve;
+decode_type(<<10, 0>>) -> curve_polygon;
+decode_type(<<11, 0>>) -> multi_curve;
+decode_type(<<12, 0>>) -> multi_surface;
+decode_type(<<13, 0>>) -> curve;
+decode_type(<<14, 0>>) -> surface;
+decode_type(<<15, 0>>) -> polyhedral_surface;
+decode_type(<<16, 0>>) -> tin;
+decode_type(<<17, 0>>) -> triangle.
 
 -spec encode_type(geometry() | geom_type()) -> binary().
 encode_type(Geometry) when is_tuple(Geometry) ->
@@ -246,7 +290,7 @@ encode_type(triangle)            -> <<17, 0>>.
 
 
 -spec decode_point_type(binary()) -> point_type().
-decode_point_type(<<0,0>>) -> '2d';
+decode_point_type(<<0, 0>>) -> '2d';
 decode_point_type(<<0, 64>>) -> '2dm';
 decode_point_type(<<0, 128>>) -> '3d';
 decode_point_type(<<0, 192>>) -> '3dm'.
@@ -254,8 +298,7 @@ decode_point_type(<<0, 192>>) -> '3dm'.
 -spec encode_point_type(geometry() | point_type()) -> binary().
 encode_point_type(Geometry) when is_tuple(Geometry) ->
   encode_point_type(element(2, Geometry));
-encode_point_type('2d') -> <<0,0>>;
-encode_point_type('2dm') -> <<0,64>>;
-encode_point_type('3d') -> <<0,128>>;
-encode_point_type('3dm') -> <<0,192>>.
-
+encode_point_type('2d') -> <<0, 0>>;
+encode_point_type('2dm') -> <<0, 64>>;
+encode_point_type('3d') -> <<0, 128>>;
+encode_point_type('3dm') -> <<0, 192>>.

+ 10 - 1
test/data/test_schema.sql

@@ -12,12 +12,16 @@ CREATE USER epgsql_test_md5 WITH PASSWORD 'epgsql_test_md5';
 CREATE USER epgsql_test_cleartext WITH PASSWORD 'epgsql_test_cleartext';
 CREATE USER epgsql_test_cert;
 CREATE USER epgsql_test_replication WITH REPLICATION PASSWORD 'epgsql_test_replication';
+SET password_encryption TO 'scram-sha-256';
+CREATE USER epgsql_test_scram WITH PASSWORD 'epgsql_test_scram';
+SET password_encryption TO 'md5';
 
 CREATE DATABASE epgsql_test_db1 WITH ENCODING 'UTF8';
 CREATE DATABASE epgsql_test_db2 WITH ENCODING 'UTF8';
 
 GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test;
 GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test_md5;
+GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test_scram;
 GRANT ALL ON DATABASE epgsql_test_db1 to epgsql_test_cleartext;
 GRANT ALL ON DATABASE epgsql_test_db2 to epgsql_test;
 
@@ -60,10 +64,15 @@ CREATE TABLE test_table2 (
   c_geometry geometry,
   c_cidr cidr,
   c_inet inet,
+  c_macaddr macaddr,
   c_int4range int4range,
   c_int8range int8range,
   c_json json,
-  c_jsonb jsonb);
+  c_jsonb jsonb,
+  c_tsrange tsrange,
+  c_tstzrange tstzrange,
+  c_daterange daterange
+  );
 
 -- CREATE LANGUAGE plpgsql;
 

+ 204 - 55
test/epgsql_SUITE.erl

@@ -30,14 +30,6 @@ init_per_suite(Config) ->
 all() ->
     [{group, M} || M <- modules()].
 
--ifdef(have_maps).
--define(MAPS_TESTS, [
-    connect_map
-]).
--else.
--define(MAPS_TESTS, []).
--endif.
-
 groups() ->
     Groups = [
         {connect, [parrallel], [
@@ -46,11 +38,14 @@ groups() ->
             connect_as,
             connect_with_cleartext,
             connect_with_md5,
+            connect_with_scram,
             connect_with_invalid_user,
             connect_with_invalid_password,
             connect_with_ssl,
-            connect_with_client_cert
-            | ?MAPS_TESTS
+            connect_with_client_cert,
+            connect_to_closed_port,
+            connect_map,
+            connect_proplist
         ]},
         {types, [parallel], [
             numeric_type,
@@ -65,9 +60,14 @@ groups() ->
             hstore_type,
             net_type,
             array_type,
+            record_type,
             range_type,
             range8_type,
+            date_time_range_type,
             custom_types
+        ]},
+        {generic, [parallel], [
+            with_transaction
         ]}
     ],
 
@@ -125,7 +125,12 @@ groups() ->
         set_notice_receiver,
         get_cmd_status
     ],
-    Groups ++ [{Module, [], Tests} || Module <- modules()].
+    Groups ++ [case Module of
+                   epgsql ->
+                       {Module, [], [{group, generic} | Tests]};
+                   _ ->
+                       {Module, [], Tests}
+               end || Module <- modules()].
 
 end_per_suite(_Config) ->
     ok.
@@ -158,10 +163,10 @@ end_per_group(_GroupName, _Config) ->
         }}).
 
 %% From uuid.erl in http://gitorious.org/avtobiff/erlang-uuid
-uuid_to_string(<<U0:32, U1:16, U2:16, U3:16, U4:48>>) ->
-    lists:flatten(io_lib:format(
-                    "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",
-                    [U0, U1, U2, U3, U4])).
+uuid_to_bin_string(<<U0:32, U1:16, U2:16, U3:16, U4:48>>) ->
+    iolist_to_binary(io_lib:format(
+                       "~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",
+                       [U0, U1, U2, U3, U4])).
 
 connect(Config) ->
     epgsql_ct:connect_only(Config, []).
@@ -186,6 +191,19 @@ connect_with_md5(Config) ->
         [{database, "epgsql_test_db1"}]
     ]).
 
+connect_with_scram(Config) ->
+    PgConf = ?config(pg_config, Config),
+    Ver = ?config(version, PgConf),
+    (Ver >= [10, 0])
+        andalso
+        epgsql_ct:connect_only(
+          Config,
+          [
+           "epgsql_test_scram",
+           "epgsql_test_scram",
+           [{database, "epgsql_test_db1"}]
+          ]).
+
 connect_with_invalid_user(Config) ->
     {Host, Port} = epgsql_ct:connection_data(Config),
     Module = ?config(module, Config),
@@ -240,7 +258,6 @@ connect_with_client_cert(Config) ->
         [{ssl, true}, {ssl_opts, [{keyfile, File("epgsql.key")},
                                   {certfile, File("epgsql.crt")}]}]).
 
--ifdef(have_maps).
 connect_map(Config) ->
     {Host, Port} = epgsql_ct:connection_data(Config),
     Module = ?config(module, Config),
@@ -256,7 +273,35 @@ connect_map(Config) ->
     Module:close(C),
     epgsql_ct:flush(),
     ok.
--endif.
+
+connect_proplist(Config) ->
+    {Host, Port} = epgsql_ct:connection_data(Config),
+    Module = ?config(module, Config),
+
+    Opts = [
+        {host, Host},
+        {port, Port},
+        {database, "epgsql_test_db1"},
+        {username, "epgsql_test_md5"},
+        {password, "epgsql_test_md5"}
+    ],
+    {ok, C} = Module:connect(Opts),
+    Module:close(C),
+    epgsql_ct:flush(),
+    ok.
+
+connect_to_closed_port(Config) ->
+    {Host, Port} = epgsql_ct:connection_data(Config),
+    Module = ?config(module, Config),
+    Trap = process_flag(trap_exit, true),
+    ?assertEqual({error, econnrefused},
+                 Module:connect(
+                   Host,
+                   "epgsql_test",
+                   "epgsql_test",
+                   [{port, Port + 1}, {database, "epgsql_test_db1"}])),
+    ?assertMatch({'EXIT', _, econnrefused}, receive Stop -> Stop end),
+    process_flag(trap_exit, Trap).
 
 prepared_query(Config) ->
     Module = ?config(module, Config),
@@ -532,8 +577,8 @@ describe_with_param(Config) ->
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
         {ok, S} = Module:parse(C, "select id from test_table1 where id = $1"),
-        [int4] = S#statement.types,
-        [#column{name = <<"id">>}] = S#statement.columns,
+        ?assertEqual([int4], S#statement.types),
+        ?assertMatch([#column{name = <<"id">>}], S#statement.columns),
         {ok, S} = Module:describe(C, S),
         ok = Module:close(C, S),
         ok = Module:sync(C)
@@ -637,7 +682,9 @@ numeric_type(Config) ->
     check_type(Config, int4, "1", 1, [0, 512, -2147483648, +2147483647]),
     check_type(Config, int8, "1", 1, [0, 1024, -9223372036854775808, +9223372036854775807]),
     check_type(Config, float4, "1.0", 1.0, [0.0, 1.23456, -1.23456]),
-    check_type(Config, float8, "1.0", 1.0, [0.0, 1.23456789012345, -1.23456789012345]).
+    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, "'nan'", nan, [minus_infinity, plus_infinity, nan]).
 
 character_type(Config) ->
     Alpha = unicode:characters_to_binary([16#03B1]),
@@ -645,12 +692,32 @@ character_type(Config) ->
     One   = unicode:characters_to_binary([16#10D360]),
     check_type(Config, bpchar, "'A'", $A, [1, $1, 16#7F, Alpha, Ka, One], "c_char"),
     check_type(Config, text, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]),
-    check_type(Config, varchar, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]).
+    check_type(Config, varchar, "'hi'", <<"hi">>, [<<"">>, <<"hi">>]),
+    epgsql_ct:with_connection(
+      Config,
+      fun(C) ->
+              Module = ?config(module, Config),
+              %% IOlists
+              ?assertMatch({ok, _, [{<<1087/utf8, 1088/utf8, 1080/utf8,
+                                        1074/utf8, 1077/utf8, 1090/utf8>>}]},
+                           Module:equery(C, "SELECT $1::text", [[1087,1088,1080,1074,1077,1090]])),
+              %% Deprecated casts
+              ?assertMatch({ok, _, [{<<"my_atom">>}]},
+                           Module:equery(C, "SELECT $1::varchar", [my_atom])),
+              ?assertMatch({ok, _, [{<<"12345">>}]},
+                           Module:equery(C, "SELECT $1::varchar", [12345])),
+              FloatBin = erlang:float_to_binary(1.2345),
+              ?assertMatch({ok, _, [{FloatBin}]},
+                           Module:equery(C, "SELECT $1::varchar", [1.2345])),
+              %% String bpchar
+              ?assertMatch({ok, _, [{<<"hello world">>}]},
+                           Module:equery(C, "SELECT $1::bpchar", ["hello world"]))
+      end).
 
 uuid_type(Config) ->
     check_type(Config, uuid,
-        io_lib:format("'~s'", [uuid_to_string(?UUID1)]),
-        list_to_binary(uuid_to_string(?UUID1)), []).
+               io_lib:format("'~s'", [uuid_to_bin_string(?UUID1)]),
+               uuid_to_bin_string(?UUID1), []).
 
 point_type(Config) ->
     check_type(Config, point, "'(23.15, 100)'", {23.15, 100.0}, []).
@@ -672,9 +739,9 @@ geometry_type(Config) ->
 uuid_select(Config) ->
     Module = ?config(module, Config),
     epgsql_ct:with_rollback(Config, fun(C) ->
-        U1 = uuid_to_string(?UUID1),
-        U2 = uuid_to_string(?UUID2),
-        U3 = uuid_to_string(?UUID3),
+        U1 = uuid_to_bin_string(?UUID1),
+        U2 = uuid_to_bin_string(?UUID2),
+        U3 = uuid_to_bin_string(?UUID3),
         {ok, 1} =
             Module:equery(C, "insert into test_table2 (c_varchar, c_uuid) values ('UUID1', $1)",
                    [U1]),
@@ -686,12 +753,11 @@ uuid_select(Config) ->
                    [U3]),
         Res = Module:equery(C, "select c_varchar, c_uuid from test_table2 where c_uuid = any($1)",
                     [[U1, U2]]),
-        U1Bin = list_to_binary(U1),
-        U2Bin = list_to_binary(U2),
-        {ok,[{column,<<"c_varchar">>,varchar,_,_,_},
-             {column,<<"c_uuid">>,uuid,_,_,_}],
-         [{<<"UUID1">>, U1Bin},
-          {<<"UUID2">>, U2Bin}]} = Res
+        ?assertMatch(
+           {ok,[#column{name = <<"c_varchar">>, type = varchar},
+                #column{name = <<"c_uuid">>, type = uuid}],
+            [{<<"UUID1">>, U1},
+             {<<"UUID2">>, U2}]}, Res)
     end).
 
 date_time_type(Config) ->
@@ -739,11 +805,16 @@ hstore_type(Config) ->
     check_type(Config, hstore, "''", {[]}, []),
     check_type(Config, hstore,
                "'a => 1, b => 2.0, c => null'",
-               {[{<<"c">>, null}, {<<"b">>, <<"2.0">>}, {<<"a">>, <<"1">>}]}, Values).
+               {[{<<"a">>, <<"1">>}, {<<"b">>, <<"2.0">>}, {<<"c">>, null}]}, Values).
 
 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, inet, "'127.0.0.1'", {127,0,0,1}, [{127,0,0,1}, {0,0,0,0,0,0,0,1}]).
+    check_type(Config, inet, "'127.0.0.1'", {127,0,0,1}, [{127,0,0,1}, {0,0,0,0,0,0,0,1}]),
+    %% macaddr8 available only on PG>=10
+    check_type(Config, macaddr,
+               "'FF:FF:FF:FF:FF:FF'", {255, 255, 255, 255, 255, 255},
+               [{255, 255, 255, 255, 255, 255},
+                {6, 0, 0, 0, 0, 0}]).
 
 array_type(Config) ->
     Module = ?config(module, Config),
@@ -785,17 +856,39 @@ array_type(Config) ->
         Select(jsonb, [<<"{}">>, <<"[]">>, <<"1">>, <<"1.0">>, <<"true">>, <<"\"string\"">>, <<"{\"key\": []}">>])
     end).
 
+record_type(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(Config, fun(C) ->
+        Select = fun(Sql, Expected) ->
+            {ok, _Columns, [Row]} = Module:equery(C, Sql, []),
+            ?assertMatch(Expected, Row)
+        end,
+
+        %% Simple record
+        Select("select (1,2)", {{1, 2}}),
+
+        %% Record inside other record
+        Select("select (1, (select (2,3)))", {{1, {2, 3}}}),
+
+        %% Array inside record
+        Select("select (1, '{2,3}'::int[])", {{1, [2, 3]}}),
+
+        %% Array of records inside record
+        Select("select (0, ARRAY(select (id, value) from test_table1))", {{0,[{1,<<"one">>},{2,<<"two">>}]}})
+    end).
+
 custom_types(Config) ->
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
         Module:squery(C, "drop table if exists t_foo;"),
-        Module:squery(C, "drop type foo;"),
-        {ok, [], []} = Module:squery(C, "create type foo as enum('foo', 'bar');"),
-        ok = epgsql:update_type_cache(C, [<<"foo">>]),
-        {ok, [], []} = Module:squery(C, "create table t_foo (col foo);"),
-        {ok, S} = Module:parse(C, "insert_foo", "insert into t_foo values ($1)", [foo]),
-        ok = Module:bind(C, S, ["bar"]),
-        {ok, 1} = Module:execute(C, S)
+        Module:squery(C, "drop type if exists my_type;"),
+        {ok, [], []} = Module:squery(C, "create type my_type as enum('foo', 'bar');"),
+        {ok, [my_type]} = epgsql:update_type_cache(C, [{epgsql_codec_test_enum, [foo, bar]}]),
+        {ok, [], []} = Module:squery(C, "create table t_foo (col my_type);"),
+        {ok, S} = Module:parse(C, "insert_foo", "insert into t_foo values ($1)", [my_type]),
+        ok = Module:bind(C, S, [bar]),
+        {ok, 1} = Module:execute(C, S),
+        ?assertMatch({ok, _, [{bar}]}, Module:equery(C, "SELECT col FROM t_foo"))
     end).
 
 text_format(Config) ->
@@ -804,8 +897,8 @@ text_format(Config) ->
         Select = fun(Type, V) ->
             V2 = list_to_binary(V),
             Query = "select $1::" ++ Type,
-            {ok, _Cols, [{V2}]} = Module:equery(C, Query, [V]),
-            {ok, _Cols, [{V2}]} = Module:equery(C, Query, [V2])
+            ?assertMatch({ok, _Cols, [{V2}]}, Module:equery(C, Query, [V])),
+            ?assertMatch({ok, _Cols, [{V2}]}, Module:equery(C, Query, [V2]))
         end,
         Select("numeric", "123456")
     end).
@@ -814,8 +907,8 @@ query_timeout(Config) ->
     Module = ?config(module, Config),
     epgsql_ct:with_connection(Config, fun(C) ->
         {ok, _, _} = Module:squery(C, "SET statement_timeout = 500"),
-        ?TIMEOUT_ERROR = Module:squery(C, "SELECT pg_sleep(1)"),
-        ?TIMEOUT_ERROR = Module:equery(C, "SELECT pg_sleep(2)"),
+        ?assertMatch(?TIMEOUT_ERROR, Module:squery(C, "SELECT pg_sleep(1)")),
+        ?assertMatch(?TIMEOUT_ERROR, Module:equery(C, "SELECT pg_sleep(2)")),
         {ok, _Cols, [{1}]} = Module:equery(C, "SELECT 1")
     end, []).
 
@@ -825,7 +918,7 @@ execute_timeout(Config) ->
         {ok, _, _} = Module:squery(C, "SET statement_timeout = 500"),
         {ok, S} = Module:parse(C, "select pg_sleep($1)"),
         ok = Module:bind(C, S, [2]),
-        ?TIMEOUT_ERROR = Module:execute(C, S, 0),
+        ?assertMatch(?TIMEOUT_ERROR, Module:execute(C, S, 0)),
         ok = Module:sync(C),
         ok = Module:bind(C, S, [0]),
         {ok, [{<<>>}]} = Module:execute(C, S, 0),
@@ -936,7 +1029,7 @@ listen_notify(Config) ->
 
 listen_notify_payload(Config) ->
     Module = ?config(module, Config),
-    epgsql_ct:with_min_version(Config, 9.0, fun(C) ->
+    epgsql_ct:with_min_version(Config, [9, 0], fun(C) ->
         {ok, [], []}     = Module:squery(C, "listen epgsql_test"),
         {ok, _, [{Pid}]} = Module:equery(C, "select pg_backend_pid()"),
         {ok, [], []}     = Module:squery(C, "notify epgsql_test, 'test!'"),
@@ -949,7 +1042,7 @@ listen_notify_payload(Config) ->
 
 set_notice_receiver(Config) ->
     Module = ?config(module, Config),
-    epgsql_ct:with_min_version(Config, 9.0, fun(C) ->
+    epgsql_ct:with_min_version(Config, [9, 0], fun(C) ->
         {ok, [], []}     = Module:squery(C, "listen epgsql_test"),
         {ok, _, [{Pid}]} = Module:equery(C, "select pg_backend_pid()"),
 
@@ -1016,7 +1109,7 @@ get_cmd_status(Config) ->
         {ok, 1} = Module:squery(C, "UPDATE cmd_status_t SET col=3 WHERE col=1"),
         ?assertEqual({ok, {'update', 1}}, Module:get_cmd_status(C)),
         %% Failed queries have no status
-        {error, _} = Module:squery(C, "UPDATE cmd_status_t SET col='text' WHERE col=2"),
+        {error, _} = Module:squery(C, "DELETE FROM cmd_status_t WHERE not_col=2"),
         ?assertEqual({ok, undefined}, Module:get_cmd_status(C)),
         %% if COMMIT failed, status will be 'rollback'
         {ok, [], []} = Module:squery(C, "COMMIT"),
@@ -1027,7 +1120,7 @@ get_cmd_status(Config) ->
     end).
 
 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, int4range, "int4range(10, 20)", {10, 20}, [
             {1, 58}, {-1, 12}, {-985521, 5412687}, {minus_infinity, 0},
             {984655, plus_infinity}, {minus_infinity, plus_infinity}
@@ -1035,7 +1128,7 @@ range_type(Config) ->
    end, []).
 
 range8_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, int8range, "int8range(10, 20)", {10, 20}, [
             {1, 58}, {-1, 12}, {-9223372036854775808, 5412687},
             {minus_infinity, 9223372036854775807},
@@ -1043,6 +1136,59 @@ range8_type(Config) ->
         ])
     end, []).
 
+date_time_range_type(Config) ->
+    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-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}}}])
+
+   end, []).
+
+with_transaction(Config) ->
+    Module = ?config(module, Config),
+    epgsql_ct:with_connection(
+      Config,
+      fun(C) ->
+              %% Success case
+              ?assertEqual(
+                 success, Module:with_transaction(C, fun(_) -> success end)),
+              ?assertEqual(
+                 success, Module:with_transaction(C, fun(_) -> success end,
+                                                  [{ensure_committed, true}])),
+              %% begin_opts
+              ?assertMatch(
+                 [{ok, _, [{<<"serializable">>}]},
+                  {ok, _, [{<<"on">>}]}],
+                 Module:with_transaction(
+                   C, fun(C1) ->
+                              Module:squery(C1, ("SHOW transaction_isolation; "
+                                                 "SHOW transaction_read_only"))
+                      end,
+                   [{begin_opts, "READ ONLY ISOLATION LEVEL SERIALIZABLE"}])),
+              %% ensure_committed failure
+              ?assertError(
+                 {ensure_committed_failed, rollback},
+                 Module:with_transaction(
+                   C, fun(C1) ->
+                              {error, _} = Module:squery(C1, "SELECT col FROM _nowhere_"),
+                              ok
+                      end,
+                   [{ensure_committed, true}])),
+              %% reraise
+              ?assertEqual(
+                 {rollback, my_err},
+                 Module:with_transaction(
+                   C, fun(_) -> error(my_err) end,
+                   [{reraise, false}])),
+              ?assertError(
+                 my_err,
+                 Module:with_transaction(
+                   C, fun(_) -> error(my_err) end, []))
+      end, []).
+
 %% =============================================================================
 %% Internal functions
 %% ============================================================================
@@ -1056,7 +1202,7 @@ check_type(Config, Type, In, Out, Values, Column) ->
     epgsql_ct:with_connection(Config, fun(C) ->
         Select = io_lib:format("select ~s::~w", [In, Type]),
         Res = Module:equery(C, Select),
-        {ok, [#column{type = Type}], [{Out}]} = Res,
+        ?assertMatch({ok, [#column{type = Type}], [{Out}]}, Res),
         Sql = io_lib:format("insert into test_table2 (~s) values ($1) returning ~s", [Column, Column]),
         {ok, #statement{columns = [#column{type = Type}]} = S} = Module:parse(C, Sql),
         Insert = fun(V) ->
@@ -1064,7 +1210,10 @@ check_type(Config, Type, In, Out, Values, Column) ->
             {ok, 1, [{V2}]} = Module:execute(C, S),
             case compare(Type, V, V2) of
                 true  -> ok;
-                false -> ?debugFmt("~p =/= ~p~n", [V, V2]), ?assert(false)
+                false ->
+                    error({write_read_compare_failed,
+                           iolist_to_binary(
+                             io_lib:format("~p =/= ~p~n", [V, V2]))})
             end,
             ok = Module:sync(C)
         end,
@@ -1073,8 +1222,8 @@ check_type(Config, Type, In, Out, Values, Column) ->
 
 compare(_Type, null, null)      -> true;
 compare(_Type, undefined, null) -> true;
-compare(float4, V1, V2)         -> abs(V2 - V1) < 0.000001;
-compare(float8, V1, V2)         -> abs(V2 - V1) < 0.000000000000001;
+compare(float4, V1, V2) when is_float(V1) -> abs(V2 - V1) < 0.000001;
+compare(float8, V1, V2) when is_float(V1) -> abs(V2 - V1) < 0.000000000000001;
 compare(hstore, {V1}, V2)       -> compare(hstore, V1, V2);
 compare(hstore, V1, {V2})       -> compare(hstore, V1, V2);
 compare(hstore, V1, V2)         ->

+ 10 - 22
test/epgsql_cast.erl

@@ -11,7 +11,6 @@
 -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([close/2, close/3, sync/1]).
--export([with_transaction/2]).
 -export([receive_result/2, sync_on_error/2]).
 
 -include("epgsql.hrl").
@@ -20,29 +19,31 @@
 
 connect(Opts) ->
     Ref = epgsqla:connect(Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Opts) ->
     Ref = epgsqla:connect(Host, Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Username, Opts) ->
     Ref = epgsqla:connect(Host, Username, Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Username, Password, Opts) ->
     Ref = epgsqla:connect(Host, Username, Password, Opts),
     %% TODO connect timeout
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
-await_connect(Ref) ->
+await_connect(Ref, Opts0) ->
+    Opts = epgsql:to_map(Opts0),
+    Timeout = maps:get(timeout, Opts, 5000),
     receive
         {C, Ref, connected} ->
             {ok, C};
         {_C, Ref, Error = {error, _}} ->
-            Error;
-        {'EXIT', _C, _Reason} ->
-            {error, closed}
+            Error
+    after Timeout ->
+            error(timeout)
     end.
 
 close(C) ->
@@ -143,19 +144,6 @@ sync(C) ->
     Ref = epgsqla:sync(C),
     receive_result(C, Ref).
 
-%% misc helper functions
-with_transaction(C, F) ->
-    try {ok, [], []} = squery(C, "BEGIN"),
-        R = F(C),
-        {ok, [], []} = squery(C, "COMMIT"),
-        R
-    catch
-        _:Why ->
-            squery(C, "ROLLBACK"),
-            %% TODO hides error stacktrace
-            {rollback, Why}
-    end.
-
 receive_result(C, Ref) ->
     %% TODO timeout
     receive

+ 19 - 0
test/epgsql_codec_test_enum.erl

@@ -0,0 +1,19 @@
+-module(epgsql_codec_test_enum).
+-behaviour(epgsql_codec).
+
+-export([init/2, names/0, encode/3, decode/3]).
+
+
+init(Choices, _) -> Choices.
+
+names() ->
+    [my_type].
+
+encode(Atom, my_type, Choices) ->
+    true = lists:member(Atom, Choices),
+    atom_to_binary(Atom, utf8).
+
+decode(Bin, my_type, Choices) ->
+    Atom = binary_to_existing_atom(Bin, utf8),
+    true = lists:member(Atom, Choices),
+    Atom.

+ 10 - 9
test/epgsql_ct.erl

@@ -64,15 +64,16 @@ with_rollback(Config, F) ->
       end).
 
 with_min_version(Config, Min, F, Args) ->
-    Module = ?config(module, Config),
-    epgsql_ct:with_connection(Config, fun(C) ->
-        {ok, Bin} = Module:get_parameter(C, <<"server_version">>),
-        {ok, [{float, 1, Ver} | _], _} = erl_scan:string(binary_to_list(Bin)),
-        case Ver >= Min of
-            true  -> F(C);
-            false -> ?debugFmt("skipping test requiring PostgreSQL >= ~.2f~n", [Min])
-        end
-    end, Args).
+    PgConf = ?config(pg_config, Config),
+    Ver = ?config(version, PgConf),
+
+    case Ver >= Min of
+        true ->
+            epgsql_ct:with_connection(Config, F, Args);
+        false ->
+            ?debugFmt("skipping test requiring PostgreSQL >= ~p, but we have ~p ~p",
+                      [Min, Ver, Config])
+    end.
 
 %% flush mailbox
 flush() ->

+ 33 - 2
test/epgsql_cth.erl

@@ -51,6 +51,7 @@ start_postgres() ->
     pipe([
         fun find_utils/1,
         fun init_database/1,
+        fun get_version/1,
         fun write_postgresql_config/1,
         fun copy_certs/1,
         fun write_pg_hba_config/1,
@@ -101,7 +102,8 @@ start_postgresql(Config) ->
             [{stderr,
               fun(_, _, Msg) ->
                   ct:pal(info, "postgres: ~s", [Msg])
-              end}]),
+              end},
+             {env, [{"LANGUAGE", "en"}]}]),
         loop(I)
     end),
     ConfigR = [
@@ -151,6 +153,15 @@ init_database(Config) ->
     {ok, _} = exec:run(Initdb ++ " --locale en_US.UTF8 " ++ PgDataDir, [sync,stdout,stderr]),
     [{datadir, PgDataDir}|Config].
 
+get_version(Config) ->
+    Datadir = ?config(datadir, Config),
+    VersionFile = filename:join(Datadir, "PG_VERSION"),
+    {ok, VersionFileData} = file:read_file(VersionFile),
+    VersionBin = list_to_binary(string:strip(binary_to_list(VersionFileData), both, $\n)),
+    Version = lists:map(fun erlang:binary_to_integer/1,
+                         binary:split(VersionBin, <<".">>, [global])),
+    [{version, Version} | Config].
+
 write_postgresql_config(Config) ->
     PgDataDir = ?config(datadir, Config),
 
@@ -158,6 +169,7 @@ write_postgresql_config(Config) ->
         "ssl = on\n",
         "ssl_ca_file = 'root.crt'\n",
         "lc_messages = 'en_US.UTF-8'\n",
+        "fsync = off\n",
         "wal_level = 'logical'\n",
         "max_replication_slots = 15\n",
         "max_wal_senders = 15"
@@ -185,6 +197,7 @@ copy_certs(Config) ->
 
 write_pg_hba_config(Config) ->
     PgDataDir = ?config(datadir, Config),
+    Version = ?config(version, Config),
 
     User = os:getenv("USER"),
     PGConfig = [
@@ -197,7 +210,25 @@ write_pg_hba_config(Config) ->
         "host    epgsql_test_db1 epgsql_test_md5         127.0.0.1/32    md5\n",
         "host    epgsql_test_db1 epgsql_test_cleartext   127.0.0.1/32    password\n",
         "hostssl epgsql_test_db1 epgsql_test_cert        127.0.0.1/32    cert clientcert=1\n",
-        "host    replication     epgsql_test_replication 127.0.0.1/32    trust"
+        "host    template1       ", User, "              ::1/128    trust\n",
+        "host    ", User, "      ", User, "              ::1/128    trust\n",
+        "hostssl postgres        ", User, "              ::1/128    trust\n",
+        "host    epgsql_test_db1 ", User, "              ::1/128    trust\n",
+        "host    epgsql_test_db1 epgsql_test             ::1/128    trust\n",
+        "host    epgsql_test_db1 epgsql_test_md5         ::1/128    md5\n",
+        "host    epgsql_test_db1 epgsql_test_cleartext   ::1/128    password\n",
+        "hostssl epgsql_test_db1 epgsql_test_cert        ::1/128    cert clientcert=1\n" |
+        case Version >= [10] of
+            true ->
+                %% See
+                %% https://www.postgresql.org/docs/10/static/release-10.html
+                %% "Change how logical replication uses pg_hba.conf"
+                ["host    epgsql_test_db1 epgsql_test_replication 127.0.0.1/32    trust\n",
+                 %% scram auth method only available on PG >= 10
+                 "host    epgsql_test_db1 epgsql_test_scram       127.0.0.1/32    scram-sha-256\n"];
+            false ->
+                ["host    replication     epgsql_test_replication 127.0.0.1/32    trust\n"]
+        end
     ],
     FilePath = filename:join(PgDataDir, "pg_hba.conf"),
     ok = file:write_file(FilePath, PGConfig),

+ 11 - 24
test/epgsql_incremental.erl

@@ -11,7 +11,6 @@
 -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([close/2, close/3, sync/1]).
--export([with_transaction/2]).
 
 -include("epgsql.hrl").
 
@@ -19,28 +18,30 @@
 
 connect(Opts) ->
     Ref = epgsqli:connect(Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Opts) ->
     Ref = epgsqli:connect(Host, Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Username, Opts) ->
     Ref = epgsqli:connect(Host, Username, Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
 connect(Host, Username, Password, Opts) ->
     Ref = epgsqli:connect(Host, Username, Password, Opts),
-    await_connect(Ref).
+    await_connect(Ref, Opts).
 
-await_connect(Ref) ->
+await_connect(Ref, Opts0) ->
+    Opts = epgsql:to_map(Opts0),
+    Timeout = maps:get(timeout, Opts, 5000),
     receive
         {C, Ref, connected} ->
             {ok, C};
         {_C, Ref, Error = {error, _}} ->
-            Error;
-        {'EXIT', _C, _Reason} ->
-            {error, closed}
+            Error
+    after Timeout ->
+            error(timeout)
     end.
 
 close(C) ->
@@ -147,19 +148,6 @@ sync(C) ->
     Ref = epgsqli:sync(C),
     receive_atom(C, Ref, ok, ok).
 
-%% misc helper functions
-with_transaction(C, F) ->
-    try {ok, [], []} = squery(C, "BEGIN"),
-        R = F(C),
-        {ok, [], []} = squery(C, "COMMIT"),
-        R
-    catch
-        _:Why ->
-            squery(C, "ROLLBACK"),
-            %% TODO hides error stacktrace
-            {rollback, Why}
-    end.
-
 %% -- internal functions --
 
 receive_result(C, Ref, Result) ->
@@ -233,8 +221,7 @@ receive_describe(C, Ref, Statement = #statement{}) ->
         {C, Ref, {types, Types}} ->
             receive_describe(C, Ref, Statement#statement{types = Types});
         {C, Ref, {columns, Columns}} ->
-            Columns2 = [Col#column{format = epgsql_wire:format(Col#column.type)} || Col <- Columns],
-            {ok, Statement#statement{columns = Columns2}};
+            {ok, Statement#statement{columns = Columns}};
         {C, Ref, no_data} ->
             {ok, Statement#statement{columns = []}};
         {C, Ref, Error = {error, _}} ->

+ 1 - 2
test/epgsql_replication_SUITE.erl

@@ -69,8 +69,7 @@ replication_test_run(Config, Callback) ->
                 fun(C2) ->
                     [{ok, 1},{ok, 1}] = Module:squery(C2,
                         "insert into test_table1 (id, value) values (5, 'five');delete from test_table1 where id = 5;")
-                end,
-                "epgsql_test_db1"),
+                end),
 
             Module:start_replication(C, "epgsql_test", Callback, {C, self()}, "0/0"),
             ok = receive_replication_msgs(