Browse Source

Add `return` and `nulls` options to hstore codec

`return` controls output format (map / jiffy / prolist) and
`nulls` controls NULL terms
Sergey Prokhorov 5 years ago
parent
commit
2492e2d63e
4 changed files with 103 additions and 32 deletions
  1. 8 2
      README.md
  2. 0 2
      rebar.config
  3. 58 27
      src/datatypes/epgsql_codec_hstore.erl
  4. 37 1
      test/epgsql_SUITE.erl

+ 8 - 2
README.md

@@ -465,8 +465,8 @@ PG type       | Representation
   record      | `{int2, time, text, ...}` (decode only)
   point       |  `{10.2, 100.12}`
   int4range   | `[1,5)`
-  hstore      | `{[ {binary(), binary() \| null} ]}`
-  json/jsonb  | `<<"{ \"key\": [ 1, 1.0, true, \"string\" ] }">>` (see below for codec details)
+  hstore      | `{[ {binary(), binary() \| null} ]}` (configurable)
+  json/jsonb  | `<<"{ \"key\": [ 1, 1.0, true, \"string\" ] }">>` (configurable)
   uuid        | `<<"123e4567-e89b-12d3-a456-426655440000">>`
   inet        | `inet:ip_address()`
   cidr        | `{ip_address(), Mask :: 0..32}`
@@ -487,6 +487,12 @@ and `plus_infinity`
 `tsrange`, `tstzrange`, `daterange` are range types for `timestamp`, `timestamptz` and `date`
 respectively. They can return `empty` atom as the result from a database if bounds are equal
 
+`hstore` type can take map or jiffy-style objects as input. Output can be tuned by
+providing `return :: map | jiffy | proplist` option to choose the format to which
+hstore should be decoded. `nulls :: [atom(), ...]` option can be used to select the
+terms which should be interpreted as SQL `NULL` - semantics is the same as
+for `connect/1` `nulls` option.
+
 `json` and `jsonb` types can optionally use a custom JSON encoding/decoding module to accept
 and return erlang-formatted JSON. The module must implement the callbacks in `epgsql_codec_json`,
 which most popular open source JSON parsers will already, and you can specify it in the codec

+ 0 - 2
rebar.config

@@ -1,7 +1,5 @@
 %% -*- mode: erlang -*-
 
-{eunit_opts, [verbose]}.
-
 {cover_enabled, true}.
 
 {edoc_opts, [{preprocess, true}]}.

+ 58 - 27
src/datatypes/epgsql_codec_hstore.erl

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

+ 37 - 1
test/epgsql_SUITE.erl

@@ -900,6 +900,7 @@ misc_type(Config) ->
     check_type(Config, bytea, "E'\001\002'", <<1,2>>, [<<>>, <<0,128,255>>]).
 
 hstore_type(Config) ->
+    Module = ?config(module, Config),
     Values = [
         {[]},
         {[{null, null}]},
@@ -915,7 +916,42 @@ hstore_type(Config) ->
     check_type(Config, hstore, "''", {[]}, []),
     check_type(Config, hstore,
                "'a => 1, b => 2.0, c => null'",
-               {[{<<"a">>, <<"1">>}, {<<"b">>, <<"2.0">>}, {<<"c">>, null}]}, Values).
+               {[{<<"a">>, <<"1">>}, {<<"b">>, <<"2.0">>}, {<<"c">>, null}]}, Values),
+    epgsql_ct:with_connection(
+      Config,
+      fun(C) ->
+              %% Maps as input
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [maps:from_list(KV)]),
+                   ?assert(compare(hstore, Res, Jiffy))
+               end || {KV} = Jiffy <- Values],
+              %% Maps as output
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => map}}]),
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [maps:from_list(KV)]),
+                   HstoreMap = maps:from_list([{format_hstore_key(K), format_hstore_value(V)} || {K, V} <- KV]),
+                   ?assertEqual(HstoreMap, Res)
+               end || {KV} <- Values],
+              %% Proplist as output
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => proplist}}]),
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [Jiffy]),
+                   HstoreProplist = [{format_hstore_key(K), format_hstore_value(V)} || {K, V} <- KV],
+                   ?assertEqual(lists:sort(HstoreProplist), lists:sort(Res))
+               end || {KV} = Jiffy <- Values],
+              %% Custom nulls
+              Nulls = [nil, 'NULL', aaaa],
+              {ok, [hstore]} = epgsql:update_type_cache(
+                                 C, [{epgsql_codec_hstore, #{return => map,
+                                                             nulls => Nulls}}]),
+              K = <<"k">>,
+              [begin
+                   {ok, _, [{Res}]} = Module:equery(C, "select $1::hstore", [#{K => V}]),
+                   ?assertEqual(#{K => nil}, Res)
+               end || V <- Nulls]
+      end).
 
 net_type(Config) ->
     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}]),