Browse Source

Merge pull request #153 from seriyps/pluggable-types

Pluggable types
Sergey Prokhorov 7 years ago
parent
commit
ed4177b98e
40 changed files with 1832 additions and 909 deletions
  1. 20 3
      README.md
  2. 4 4
      include/epgsql.hrl
  3. 13 61
      include/epgsql_geometry.hrl
  4. 94 0
      pluggable_types.md
  5. 2 1
      rebar.config
  6. 9 11
      src/commands/epgsql_cmd_connect.erl
  7. 21 9
      src/commands/epgsql_cmd_describe_statement.erl
  8. 1 1
      src/commands/epgsql_cmd_execute.erl
  9. 22 9
      src/commands/epgsql_cmd_parse.erl
  10. 61 0
      src/commands/epgsql_cmd_update_type_cache.erl
  11. 31 0
      src/datatypes/epgsql_codec_boolean.erl
  12. 32 0
      src/datatypes/epgsql_codec_bpchar.erl
  13. 55 0
      src/datatypes/epgsql_codec_datetime.erl
  14. 57 0
      src/datatypes/epgsql_codec_float.erl
  15. 32 0
      src/datatypes/epgsql_codec_geometric.erl
  16. 76 0
      src/datatypes/epgsql_codec_hstore.erl
  17. 40 0
      src/datatypes/epgsql_codec_integer.erl
  18. 90 0
      src/datatypes/epgsql_codec_intrange.erl
  19. 37 0
      src/datatypes/epgsql_codec_json.erl
  20. 74 0
      src/datatypes/epgsql_codec_net.erl
  21. 23 0
      src/datatypes/epgsql_codec_noop.erl
  22. 28 0
      src/datatypes/epgsql_codec_postgis.erl
  23. 51 0
      src/datatypes/epgsql_codec_text.erl
  24. 37 0
      src/datatypes/epgsql_codec_uuid.erl
  25. 79 79
      src/epgsql.erl
  26. 293 324
      src/epgsql_binary.erl
  27. 69 0
      src/epgsql_codec.erl
  28. 1 1
      src/epgsql_command.erl
  29. 11 47
      src/epgsql_fdatetime.erl
  30. 12 9
      src/epgsql_idatetime.erl
  31. 195 0
      src/epgsql_oid_db.erl
  32. 12 12
      src/epgsql_sock.erl
  33. 0 207
      src/epgsql_types.erl
  34. 103 76
      src/epgsql_wire.erl
  35. 17 15
      src/epgsqla.erl
  36. 3 3
      src/epgsqli.erl
  37. 46 3
      src/ewkb.erl
  38. 1 0
      test/data/test_schema.sql
  39. 61 34
      test/epgsql_SUITE.erl
  40. 19 0
      test/epgsql_codec_test_enum.erl

+ 20 - 3
README.md

@@ -69,6 +69,7 @@ see `CHANGES` for full list.
     {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
     
 -spec connect(host(), string(), string(), [connect_option()] | map())
@@ -78,7 +79,7 @@ see `CHANGES` for full list.
 %% `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) -> ...
 ```
@@ -388,13 +389,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}`
@@ -410,11 +415,16 @@ PG type       | Representation
   int4range   | `[1,5)`
   hstore      | `{list({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()`
 
 
   `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`
 
@@ -504,6 +514,13 @@ Parameter's value may change during connection's lifetime.
 
 See [streaming.md](streaming.md).
 
+## Pluggable commands
+
+TODO: docs
+
+## Pluggable datatype codecs
+
+See [pluggable_types.md](pluggable_types.md)
 
 ## Mailing list
 

+ 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, {

+ 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)].

+ 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!

+ 2 - 1
rebar.config

@@ -1,4 +1,5 @@
-{erl_opts, [{platform_define, "^[0-9]+", have_maps}]}.
+{erl_opts, [{platform_define, "^[0-9]+", have_maps},
+            {platform_define, "^(1[89])|^([2-9][0-9])", 'FAST_MAPS'}]}. % Erlang >=18
 
 {eunit_opts, [verbose]}.
 

+ 9 - 11
src/commands/epgsql_cmd_connect.erl

@@ -5,13 +5,15 @@
 -module(epgsql_cmd_connect).
 -behaviour(epgsql_command).
 -export([init/1, execute/2, handle_message/4]).
--export_type([response/0]).
+-export_type([response/0, connect_error/0]).
 
 -type response() :: connected
-                  | {error,
-                     invalid_authorization_specification
-                     | invalid_password
-                     | epgsql:query_error()}.
+                  | {error, connect_error()}.
+-type connect_error() ::
+        invalid_authorization_specification
+      | invalid_password
+      | {unsupported_auth_method, integer()}
+      | epgsql:query_error().
 
 -include("epgsql.hrl").
 -include("protocol.hrl").
@@ -145,12 +147,8 @@ handle_message(?CANCELLATION_KEY, <<Pid:?int32, Key:?int32>>, Sock, _State) ->
 
 %% ReadyForQuery
 handle_message(?READY_FOR_QUERY, _, Sock, _State) ->
-    %% TODO decode dates to now() format
-    case epgsql_sock:get_parameter_internal(<<"integer_datetimes">>, Sock) of
-        <<"on">>  -> put(datetime_mod, epgsql_idatetime);
-        <<"off">> -> put(datetime_mod, epgsql_fdatetime)
-    end,
-    Sock1 = epgsql_sock:set_attr(codec, epgsql_binary:new_codec([]), Sock),
+    Codec = epgsql_binary:new_codec(Sock, []),
+    Sock1 = epgsql_sock:set_attr(codec, Codec, Sock),
     {finish, connected, connected, Sock1};
 
 

+ 21 - 9
src/commands/epgsql_cmd_describe_statement.erl

@@ -14,7 +14,8 @@
 
 -record(desc_stmt,
         {name :: iodata(),
-         parameter_descr}).
+         parameter_typenames = [],
+         parameter_descr = []}).
 
 init(Name) ->
     #desc_stmt{name = Name}.
@@ -30,19 +31,30 @@ execute(Sock, #desc_stmt{name = Name} = St) ->
 
 handle_message(?PARAMETER_DESCRIPTION, Bin, Sock, State) ->
     Codec = epgsql_sock:get_codec(Sock),
-    Types = epgsql_wire:decode_parameters(Bin, Codec),
-    Sock2 = epgsql_sock:notify(Sock, {types, Types}),
-    {noaction, Sock2, State#desc_stmt{parameter_descr = Types}};
+    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}) ->
+               #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#column.type, Codec)}
+    Columns2 = [Col#column{format = epgsql_wire:format(Col, Codec)}
                 || Col <- Columns],
-    Result = {ok, #statement{name = Name, types = Params, columns = Columns2}},
+    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}) ->
-    Result = {ok, #statement{name = Name, types = Params, columns = []}},
+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},

+ 1 - 1
src/commands/epgsql_cmd_execute.erl

@@ -8,7 +8,7 @@
 
 -type response() :: {ok, Count :: non_neg_integer(), Rows :: [tuple()]}
                   | {ok, Count :: non_neg_integer()}
-                  | {ok, Rows :: [tuple()]}
+                  | {ok | partial, Rows :: [tuple()]}
                   | {error, epgsql:query_error()}.
 
 -include("epgsql.hrl").

+ 22 - 9
src/commands/epgsql_cmd_parse.erl

@@ -17,8 +17,10 @@
         {name :: iodata(),
          sql :: iodata(),
          types :: [atom()],
-         parameter_descr = []}).
+         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}.
 
@@ -38,19 +40,30 @@ handle_message(?PARSE_COMPLETE, <<>>, Sock, _State) ->
     {noaction, Sock};
 handle_message(?PARAMETER_DESCRIPTION, Bin, Sock, State) ->
     Codec = epgsql_sock:get_codec(Sock),
-    Types = epgsql_wire:decode_parameters(Bin, Codec),
-    Sock2 = epgsql_sock:notify(Sock, {types, Types}),
-    {noaction, Sock2, State#parse{parameter_descr = Types}};
+    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}) ->
+               #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#column.type, Codec)}
+    Columns2 = [Col#column{format = epgsql_wire:format(Col, Codec)}
                 || Col <- Columns],
-    Result = {ok, #statement{name = Name, types = Params, columns = Columns2}},
+    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}) ->
-    Result = {ok, #statement{name = Name, types = Params, columns = []}},
+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},

+ 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.

+ 32 - 0
src/datatypes/epgsql_codec_bpchar.erl

@@ -0,0 +1,32 @@
+%%% @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.
+
+decode(<<C:1/big-unsigned-unit:8>>, _, _) -> C;
+decode(Bin, bpchar, _) -> Bin.
+
+decode_text(V, _, _) -> V.

+ 55 - 0
src/datatypes/epgsql_codec_datetime.erl

@@ -0,0 +1,55 @@
+%%% @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 logick out from fdatetime/idatetime; make them more
+%% low-level
+encode(Val, Type, Mod) ->
+    Mod:encode(Type, Val).
+
+decode(Bin, Type, Mod) ->
+    Mod: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.

+ 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.

+ 79 - 79
src/epgsql.erl

@@ -30,8 +30,11 @@
          to_proplist/1]).
 
 -export_type([connection/0, connect_option/0, connect_opts/0,
-              connect_error/0, query_error/0,
-              sql_query/0, column/0, bind_param/0, typed_param/0,
+              connect_error/0, query_error/0, sql_query/0, column/0,
+              type_name/0, epgsql_type/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]).
 
@@ -50,6 +53,7 @@
     {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).
@@ -64,46 +68,31 @@
           ssl_opts => [ssl:ssl_option()],
           timeout => timeout(),
           async => pid(),
+          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 squery_row() :: tuple(). % tuple of binary().
@@ -111,7 +100,7 @@
 -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
+    {ok, Count :: non_neg_integer(), ColumnsDescription :: [column()], RowsValues :: [RowType]}. % update/insert/delete + returning
 -type error_reply() :: {error, query_error()}.
 -type reply(RowType) :: ok_reply(RowType) | error_reply().
 -type lsn() :: integer().
@@ -161,35 +150,41 @@ connect(C, Host, Username, Password, Opts0) ->
     case epgsql_sock:sync_command(
            C, epgsql_cmd_connect, {Host, Username, Password, Opts}) 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, Opts),
+            {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, proplists:get_value(replication, Opts), proplists:get_value(codecs, Opts)).
+
+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,7 +208,7 @@ 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) ->
     epgsql_sock:sync_command(Connection, epgsql_cmd_squery, SqlQuery).
@@ -231,17 +226,19 @@ equery(C, Sql, Parameters) ->
             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),
-            epgsql_sock:sync_command(C, epgsql_cmd_equery, {S, Typed_Parameters});
+            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} ->
@@ -261,7 +258,7 @@ 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, epgsql_sock:sync_command(
@@ -273,7 +270,7 @@ bind(C, Statement, Parameters) ->
     bind(C, Statement, "", Parameters).
 
 -spec bind(connection(), #statement{}, string(), [bind_param()]) ->
-                  ok | {error, query_error()}.
+                  epgsql_cmd_bind:response().
 bind(C, Statement, PortalName, Parameters) ->
     sync_on_error(
       C,
@@ -288,41 +285,43 @@ 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) ->
     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) ->
     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, 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, 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) ->
     epgsql_sock:sync_command(C, epgsql_cmd_close, {Type, Name}).
 
+-spec sync(connection()) -> epgsql_cmd_sync:response().
 sync(C) ->
     epgsql_sock:sync_command(C, epgsql_cmd_sync, []).
 
@@ -392,13 +391,14 @@ sync_on_error(C, Error = {error, _}) ->
 sync_on_error(_C, R) ->
     R.
 
--spec standby_status_update(connection(), lsn(), lsn()) -> ok | error_reply().
+-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}).
 
--spec start_replication(connection(), string(), Callback, cb_state(), string(), string()) -> ok | error_reply() when
-    Callback :: module() | pid().
+-spec start_replication(connection(), string(), Callback, cb_state(), string(), string()) -> Response when
+      Response :: epgsql_cmd_start_replication:response(),
+      Callback :: module() | pid().
 %% @doc instructs Postgres server to start streaming WAL for logical replication
 %% where
 %% `Connection'      - connection in replication mode

+ 293 - 324
src/epgsql_binary.erl

@@ -1,347 +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]).
-
--export_type([codec/0]).
+-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]).
 
--record(codec, {
-    type2oid = [],
-    oid2type = []
-}).
+-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.
 
--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).
-
--spec new_codec(list()) -> codec().
-new_codec([]) -> #codec{}.
-
--spec update_type_cache(list(), 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}.
-
--spec oid2type(integer(), codec()) -> Type | {unknown_oid, integer()} when
-      Type :: atom() | {array, atom()}.
-oid2type(Oid, #codec{oid2type = Oid2Type}) ->
-    case epgsql_types:oid2type(Oid) of
-        {unknown_oid, _} ->
-            proplists:get_value(Oid, Oid2Type, {unknown_oid, Oid});
-        Type -> Type
+-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.
 
--spec type2oid(Type, codec()) -> integer() | {unknown_type, Type} when
-      Type :: atom() | {array, atom()}.
-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.
 
-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),
+-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{oid_db = Db} = Codec) ->
+    Value =
+        case epgsql_oid_db:find_by_oid(Oid, Db) of
+            undefined -> ValueBin;
+            Type ->
+                {Name, Mod, State} = epgsql_oid_db:type_to_codec_entry(Type),
+                Mod:decode(ValueBin, Name, State)
+        end,
+    [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_value(Value, {Mod, Name, State}) ->
+    Payload = Mod:encode(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_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},
+     {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},
+     {uuid, 2950, 2951},
+     {varchar, 1043, 1015}].

+ 69 - 0
src/epgsql_codec.erl

@@ -0,0 +1,69 @@
+%%% @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]).
+
+-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}.

+ 1 - 1
src/epgsql_command.erl

@@ -5,7 +5,7 @@
 -module(epgsql_command).
 -export([init/2, execute/3, handle_message/5]).
 
--export_type([command/0]).
+-export_type([command/0, state/0]).
 
 -type command() :: module().
 -type state() :: any().

+ 11 - 47
src/epgsql_fdatetime.erl

@@ -14,57 +14,21 @@
 -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),
@@ -86,7 +50,7 @@ f2timestamp(N) ->
 
 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
@@ -98,7 +62,7 @@ f2timestamp2(D, T) ->
     {Date, Time}.
 
 timestamp2f({Date, Time}) ->
-    D = date2j(Date) - ?postgres_epoc_jdate,
+    D = epgsql_idatetime:date2j(Date) - ?postgres_epoc_jdate,
     D * ?secs_per_day + time2f(Time).
 
 now2f({MegaSecs, Secs, MicroSecs}) ->

+ 12 - 9
src/epgsql_idatetime.erl

@@ -3,6 +3,7 @@
 -module(epgsql_idatetime).
 
 -export([decode/2, encode/2]).
+-export([j2date/1, date2j/1]).
 
 -include("protocol.hrl").
 
@@ -24,15 +25,17 @@ 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,

+ 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
+-ifdef(FAST_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.

+ 12 - 12
src/epgsql_sock.erl

@@ -56,8 +56,10 @@
                    | {cast, pid(), reference()}
                    | {incremental, pid(), reference()}.
 
+-type tcp_socket() :: port(). %gen_tcp:socket() isn't exported prior to erl 18
+
 -record(state, {mod :: gen_tcp | ssl | undefined,
-                sock :: gen_tcp:socket() | ssl:sslsocket() | undefined,
+                sock :: tcp_socket() | ssl:sslsocket() | undefined,
                 data = <<>>,
                 backend :: {Pid :: integer(), Key :: integer()} | undefined,
                 handler = on_message :: on_message | on_replication | undefined,
@@ -87,11 +89,11 @@ close(C) when is_pid(C) ->
     catch gen_server:cast(C, stop),
     ok.
 
--spec sync_command(epgsql:conection(), epgsql_command:command(), any()) -> any().
+-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:conection(), cast | incremental,
+-spec async_command(epgsql:connection(), cast | incremental,
                     epgsql_command:command(), any()) -> reference().
 async_command(C, Transport, Command, Args) ->
     Ref = make_ref(),
@@ -118,7 +120,7 @@ cancel(S) ->
 %% send()
 %% send_many()
 
--spec set_net_socket(gen_tcp | ssl, gen_tcp:socket() | ssl:sslsocket(), pg_sock()) -> pg_sock().
+-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}]),
@@ -176,10 +178,6 @@ get_parameter_internal(Name, #state{parameters = Parameters}) ->
 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) ->
     {reply, {ok, get_parameter_internal(Name, State)}, State};
 
@@ -344,18 +342,20 @@ setopts(#state{mod = Mod, sock = 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) ->
@@ -364,7 +364,7 @@ do_send(gen_tcp, Sock, Bin) ->
             ok
     catch
         error:_Error ->
-            {error,einval}
+            {error, einval}
     end;
 
 do_send(Mod, Sock, Bin) ->

+ 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}.

+ 103 - 76
src/epgsql_wire.erl

@@ -8,8 +8,8 @@
          decode_strings/1,
          decode_columns/3,
          decode_parameters/2,
-         encode/1,
-         encode/2,
+         encode_command/1,
+         encode_command/2,
          build_decoder/2,
          decode_data/2,
          decode_complete/1,
@@ -18,11 +18,14 @@
          format/2,
          encode_parameters/2,
          encode_standby_status_update/3]).
+-export_type([row_decoder/0]).
 
 -include("epgsql.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
@@ -31,20 +34,22 @@ decode_message(<<Type:8, Len:?int32, Rest/binary>> = Bin) ->
         _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
+%% @doc decode error's field
+-spec decode_fields(binary()) -> [{byte(), binary()}].
 decode_fields(Bin) ->
     decode_fields(Bin, []).
 
@@ -54,8 +59,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()) -> #error{}.
 decode_error(Bin) ->
     Fields = decode_fields(Bin),
     ErrCode = proplists:get_value($C, Fields),
@@ -111,61 +117,60 @@ lower_atom(Str) when is_binary(Str) ->
 lower_atom(Str) when is_list(Str) ->
     list_to_atom(string:to_lower(Str)).
 
-%% FIXME: return iolist
-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>>.
 
-%% Build decoder for DataRow
+%% @doc Build decoder for DataRow
+-spec build_decoder([epgsql:column()], epgsql_binary:codec()) -> row_decoder().
 build_decoder(Columns, Codec) ->
-    {Columns, Codec}.
-
-%% decode row data
-%% FIXME: use body recursion
-decode_data(Bin, {Columns, 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
-%% TODO: use body-recursion
+    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, Columns, Codec)).
+
+decode_data(_, [], [], _) -> [];
+decode_data(<<-1:?int32, Rest/binary>>, [_Dec | Decs], [_Col | Cols], Codec) ->
+    [null | decode_data(Rest, Decs, Cols, Codec)];
+decode_data(<<Len:?int32, Value:Len/binary, Rest/binary>>, [Decoder | Decs], [_Col | Cols], Codec) ->
+    [epgsql_binary:decode(Value, Decoder)
+     | decode_data(Rest, Decs, Cols, Codec)].
+
+%% @doc decode column information
+-spec decode_columns(non_neg_integer(), binary(), epgsql_binary:codec()) -> [#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).
+    [Desc | decode_columns(Count - 1, Rest2, Codec)].
 
-%% decode ParameterDescription
+%% @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) ->
-    [epgsql_binary:oid2type(Oid, Codec) || <<Oid:?int32>> <= Bin].
+    [case epgsql_binary:oid_to_info(Oid, Codec)  of
+         undefined -> {unknown_oid, Oid};
+         TypeInfo -> TypeInfo
+     end || <<Oid:?int32>> <= Bin].
 
-%% decode command complete msg
+%% @doc decode command complete msg
 decode_complete(<<"SELECT", 0>>)        -> select;
 decode_complete(<<"SELECT", _/binary>>) -> select;
 decode_complete(<<"BEGIN", 0>>)         -> 'begin';
@@ -181,7 +186,8 @@ decode_complete(Bin) ->
         [Type | _Rest]         -> lower_atom(Type)
     end.
 
-%% encode types
+
+%% @doc encode types
 encode_types(Types, Codec) ->
     encode_types(Types, 0, <<>>, Codec).
 
@@ -190,12 +196,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([#column{}]) -> binary().
 encode_formats(Columns) ->
     encode_formats(Columns, 0, <<>>).
 
@@ -205,44 +212,64 @@ encode_formats([], Count, Acc) ->
 encode_formats([#column{format = Format} | T], Count, Acc) ->
     encode_formats(T, Count + 1, <<Acc/binary, Format:?int16>>).
 
-format(Type, _Codec) ->
-    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

+ 17 - 15
src/epgsqla.erl

@@ -19,7 +19,7 @@
          close/2, close/3,
          sync/1,
          cancel/1,
-         complete_connect/2]).
+         complete_connect/3]).
 
 -include("epgsql.hrl").
 
@@ -48,9 +48,9 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), [epgsql:connect_option()]) -> reference().
 connect(C, Host, Username, Password, Opts) ->
+    Opts1 = epgsql:to_proplist(Opts),
     complete_connect(
-      C, cast(
-           C, epgsql_cmd_connect, {Host, Username, Password, epgsql:to_proplist(Opts)})).
+      C, cast(C, epgsql_cmd_connect, {Host, Username, Password, Opts1}), Opts1).
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
@@ -92,7 +92,7 @@ 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(), string(), [epgsql:epgsql_type()]) -> reference().
 parse(C, Name, Sql, Types) ->
     cast(C, epgsql_cmd_parse, {Name, Sql, Types}).
 
@@ -144,20 +144,22 @@ cancel(C) ->
 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 proplists:get_value(codecs, Opts) 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,

+ 3 - 3
src/epgsqli.erl

@@ -47,9 +47,9 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), [epgsql:connect_option()]) -> reference().
 connect(C, Host, Username, Password, Opts) ->
+    Opts1 = epgsql:to_proplist(Opts),
     epgsqla:complete_connect(
-      C, incremental(
-           C, epgsql_cmd_connect, {Host, Username, Password, epgsql:to_proplist(Opts)})).
+      C, incremental(C, epgsql_cmd_connect, {Host, Username, Password, Opts1}), Opts1).
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
@@ -91,7 +91,7 @@ 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(), string(), [epgsql:epgsql_type()]) -> reference().
 parse(C, Name, Sql, Types) ->
     incremental(C, epgsql_cmd_parse, {Name, Sql, Types}).
 

+ 46 - 3
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),
@@ -258,4 +302,3 @@ encode_point_type('2d') -> <<0,0>>;
 encode_point_type('2dm') -> <<0,64>>;
 encode_point_type('3d') -> <<0,128>>;
 encode_point_type('3dm') -> <<0,192>>.
-

+ 1 - 0
test/data/test_schema.sql

@@ -60,6 +60,7 @@ 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,

+ 61 - 34
test/epgsql_SUITE.erl

@@ -167,10 +167,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, []).
@@ -554,8 +554,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)
@@ -659,7 +659,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]),
@@ -667,12 +669,29 @@ 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]))
+      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}, []).
@@ -694,9 +713,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]),
@@ -708,12 +727,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) ->
@@ -761,11 +779,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),
@@ -811,13 +834,14 @@ 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) ->
@@ -826,8 +850,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).
@@ -1121,7 +1145,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) ->
@@ -1129,7 +1153,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,
@@ -1138,8 +1165,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)         ->

+ 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.