minato_codec (minato v0.18.6)

View Source

Erlang terms to PostgreSQL wire bytes and back, for both the binary and the text wire format.

This module is pure. It does no I/O, starts no process, reads no ETS table and keeps no state, so a codec can be tested exhaustively without a database and reused wherever the bytes come from.

The two formats

PostgreSQL will send and accept every value in either a binary or a text representation, chosen per parameter and per result column. minato implements both. Binary is what the protocol layer asks for on the types it knows; text is what makes an unknown type still usable, because any value at all can be carried as its text representation.

preferred_format/1 says which format to request for a given OID.

NULL

null is the NULL sentinel in both directions, and it is the only one.

NULL is not a value in the wire format: it is a length of -1 where the bytes would be. encode/4 therefore returns the atom null rather than iodata() when the value is null, and the caller writing the message body turns that into a length of -1. decode/4 accepts null in the same place and returns null.

Errors

Every function in this module either returns a value or raises. A codec failure is error({minato_codec, Report}), where Report is a map with type, reason and value keys.

Raising rather than returning {error, _} is deliberate: decoding a result set calls decode/4 once per cell, and a failure there means the bytes disagree with the OID the server itself announced, which is a bug and not a condition to branch on. One try around a whole row costs nothing, a tagged tuple per cell does.

Options

#{uuid_format => string | binary,
  datetime_format => datetime | microseconds,
  numeric_format => binary | float}

All three are decode-time options; encoding accepts every form regardless.

  • numeric_format defaults to binary, giving the exact decimal text PostgreSQL sent. float converts it, which loses exactness and is the right answer for an average or a ratio and the wrong one for money. A client that did this by default would be quietly lossy about the one type chosen for not being lossy, which is why it has to be asked for.
  • uuid_format defaults to string, giving the 36 character hyphenated form. binary gives the 16 raw bytes.
  • datetime_format defaults to datetime, giving {Hour, Minute, Second} and {{Year, Month, Day}, {Hour, Minute, Second}} tuples with sub-second precision discarded. microseconds is lossless and gives integers. See minato_codec_datetime for exactly how the two differ.

Types covered

bool, bytea, int2, int4, int8, float4, float8, numeric, text, varchar, json, jsonb, uuid, date, time, timestamp, timestamptz, and the array type of each of them.

Examples

1> minato_codec:encode(23, 42, binary).
<<0,0,0,42>>
2> minato_codec:decode(23, <<0,0,0,42>>, binary).
42
3> minato_codec:decode(1009, ~"{a,NULL,\"b,c\"}", text).
[<<"a">>,null,<<"b,c">>]
4> minato_codec:encode(2950, ~"0196f0a9-1c4f-7e21-9b3a-6d2c5e8f1a44", binary).
<<1,150,240,169,28,79,126,33,155,58,109,44,94,143,26,68>>

Summary

Types

A calendar date, the shape of calendar:date/0 without the range refinements.

A date and a time, the shape of calendar:datetime/0.

One column's type resolution, from decoder/3.

Which of the two PostgreSQL wire representations a call is about.

Decode-time options. See the module documentation.

The map carried by an error({minato_codec, Report}) exception.

A wall clock time, the shape of calendar:time/0 without the range refinements.

Every Erlang shape a codec in minato produces or accepts.

Functions

Raise a codec failure.

Decode one value with a decoder from decoder/3.

decode/4 with the default options.

Decode wire bytes of the type named by Oid to an Erlang term.

A decoder is the type resolution for one column, done once.

encode/4 with the default options.

Encode an Erlang term to the wire bytes of the type named by Oid.

The wire format to request for a column or parameter of this type.

Whether minato has a codec for this OID.

Flatten a value the caller offered as iodata() into a binary.

Types

date()

-type date() :: {integer(), integer(), integer()}.

A calendar date, the shape of calendar:date/0 without the range refinements.

datetime()

-type datetime() :: {date(), time()}.

A date and a time, the shape of calendar:datetime/0.

decoder()

-opaque decoder()

One column's type resolution, from decoder/3.

format()

-type format() :: binary | text.

Which of the two PostgreSQL wire representations a call is about.

opts()

-type opts() ::
          #{uuid_format => string | binary,
            numeric_format => binary | float,
            datetime_format => datetime | microseconds}.

Decode-time options. See the module documentation.

report()

-type report() :: #{type := term(), reason := term(), value := term()}.

The map carried by an error({minato_codec, Report}) exception.

time()

-type time() :: {integer(), integer(), integer()}.

A wall clock time, the shape of calendar:time/0 without the range refinements.

value()

-type value() ::
          null |
          boolean() |
          integer() |
          float() |
          binary() |
          nan | infinity | neg_infinity |
          date() |
          datetime() |
          [value()].

Every Erlang shape a codec in minato produces or accepts.

Functions

bad(Type, Reason, Value)

-spec bad(term(), term(), term()) -> no_return().

Raise a codec failure.

Exported so that the codec modules share one exception shape. Not part of the API a caller of minato uses.

decode/2

-spec decode(decoder(), binary() | null) -> value().

Decode one value with a decoder from decoder/3.

decode(Oid, Bin, Format)

-spec decode(minato_oid:oid(), binary() | null, format()) -> value().

decode/4 with the default options.

decode/4

-spec decode(minato_oid:oid(), binary() | null, format(), opts()) -> value().

Decode wire bytes of the type named by Oid to an Erlang term.

null in, null out. Raises error({minato_codec, t:report/0}) when the bytes do not fit the type.

For an OID minato has no codec for, the text format returns the bytes unchanged so the caller can parse them, and the binary format raises.

decoder(Oid, Format, Opts)

-spec decoder(minato_oid:oid(), format(), opts()) -> decoder().

A decoder is the type resolution for one column, done once.

decode/4 asks minato_oid:name/1 what a type is on every value, which for a result set is the same answer per column repeated once per row: a thousand rows of two columns resolve the same two types two thousand times. The column types are fixed by the RowDescription before the first row arrives, so they can be resolved there instead, and decode/2 applies the answer.

encode(Oid, Value, Format)

-spec encode(minato_oid:oid(), term(), format()) -> iodata() | null.

encode/4 with the default options.

encode/4

-spec encode(minato_oid:oid(), term(), format(), opts()) -> iodata() | null.

Encode an Erlang term to the wire bytes of the type named by Oid.

Returns the atom null for a null value, because NULL is carried by a length of -1 and has no bytes of its own. Raises error({minato_codec, t:report/0}) when the term does not fit the type.

For an OID minato has no codec for, the text format passes iodata() through unchanged and the binary format raises: minato cannot invent a binary representation for a type it does not know, but it can always let the server parse the text one.

preferred_format(Oid)

-spec preferred_format(minato_oid:oid()) -> format().

The wire format to request for a column or parameter of this type.

binary for every type minato has a codec for, text for everything else, so that an unrecognised type still arrives in a form the caller can read.

supports(Oid)

-spec supports(minato_oid:oid()) -> boolean().

Whether minato has a codec for this OID.

to_binary(Type, Value)

-spec to_binary(term(), term()) -> binary().

Flatten a value the caller offered as iodata() into a binary.

Written out rather than handed to iolist_to_binary/1 so that a caller who passes something that is not iodata() gets a codec failure naming the type instead of a bare badarg. Binaries, which is what every hot path passes, never reach it.

Exported so that the codec modules share one implementation. Not part of the API a caller of minato uses.